flexops 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +226 -58
- data/lib/flexops/client.rb +6 -7
- data/lib/flexops/errors.rb +14 -2
- data/lib/flexops/hazmat_classification.rb +34 -0
- data/lib/flexops/http.rb +76 -44
- 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 +51 -20
- data/lib/flexops/resources/rules.rb +18 -2
- data/lib/flexops/resources/shipping.rb +72 -9
- 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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
module FlexOps
|
|
10
10
|
module Resources
|
|
11
|
+
# Shipping insurance. Answers use the { success, data, message, errorCode } envelope.
|
|
11
12
|
class Insurance
|
|
12
13
|
def initialize(http, ws_id_proc)
|
|
13
14
|
@http = http
|
|
@@ -18,24 +19,33 @@ module FlexOps
|
|
|
18
19
|
@http.get("#{ws_path}/insurance/providers")
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def get_quote(
|
|
22
|
-
body = {
|
|
23
|
-
|
|
22
|
+
def get_quote(declared_value:, carrier_code:, tracking_number: nil, provider: nil)
|
|
23
|
+
body = { declaredValue: declared_value, carrierCode: carrier_code,
|
|
24
|
+
trackingNumber: tracking_number, provider: provider }.compact
|
|
24
25
|
@http.post("#{ws_path}/insurance/quote", body: body)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# Buys a policy and charges the premium to the workspace wallet. A shipment that already has a
|
|
29
|
+
# policy in force is refused with 409 ALREADY_EXISTS.
|
|
30
|
+
# Pass an idempotency_key you store with the purchase to make your own retry safe: after an
|
|
31
|
+
# error or a timeout, retry with the same key and the Gateway returns the first result instead
|
|
32
|
+
# of buying again. Without one, the SDK's own retries still share a key, but a new call gets a new key.
|
|
33
|
+
def purchase(tracking_number:, carrier_code:, declared_value:, coverage_tier: nil, provider: nil,
|
|
34
|
+
idempotency_key: nil)
|
|
35
|
+
body = { trackingNumber: tracking_number, carrierCode: carrier_code, declaredValue: declared_value,
|
|
36
|
+
coverageTier: coverage_tier, provider: provider }.compact
|
|
37
|
+
@http.post("#{ws_path}/insurance/policies", body: body,
|
|
38
|
+
headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def void(policy_id)
|
|
34
42
|
@http.delete("#{ws_path}/insurance/policies/#{policy_id}")
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
def file_claim(policy_id
|
|
38
|
-
|
|
45
|
+
def file_claim(policy_id:, claim_reason:, claim_amount:, evidence_json: nil)
|
|
46
|
+
body = { policyId: policy_id, claimReason: claim_reason, claimAmount: claim_amount,
|
|
47
|
+
evidenceJson: evidence_json }.compact
|
|
48
|
+
@http.post("#{ws_path}/insurance/claims", body: body)
|
|
39
49
|
end
|
|
40
50
|
|
|
41
51
|
private
|
|
@@ -15,10 +15,6 @@ module FlexOps
|
|
|
15
15
|
@http = http
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def post_asn_receipt(receipt)
|
|
19
|
-
@http.post("#{PROXY}/api/v1/Inventory/postNewAsnReceipt", body: receipt)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
18
|
def get_warehouse_snapshot(params = nil)
|
|
23
19
|
@http.get("#{PROXY}/api/v1/Inventory/getWarehouseInventorySnapshot", query: params)
|
|
24
20
|
end
|
|
@@ -30,10 +26,6 @@ module FlexOps
|
|
|
30
26
|
def get_part_numbers(params = nil)
|
|
31
27
|
@http.get("#{PROXY}/api/v1/Inventory/getPartNumberList", query: params)
|
|
32
28
|
end
|
|
33
|
-
|
|
34
|
-
def update_inventory(data)
|
|
35
|
-
@http.post("#{PROXY}/api/v2/Inventory/postCustomerInventoryUpdate", body: data)
|
|
36
|
-
end
|
|
37
29
|
end
|
|
38
30
|
end
|
|
39
31
|
end
|
|
@@ -15,10 +15,6 @@ module FlexOps
|
|
|
15
15
|
@http = http
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def create(order)
|
|
19
|
-
@http.post("#{BASE}/postNewOrder", body: order)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
18
|
def get_new_orders(params = nil)
|
|
23
19
|
@http.get("#{BASE}/getNewOrderList", query: params)
|
|
24
20
|
end
|
|
@@ -39,10 +35,6 @@ module FlexOps
|
|
|
39
35
|
@http.get("#{BASE}/getIndividualOrderStatusByOrderNumber", query: { orderNumber: order_number })
|
|
40
36
|
end
|
|
41
37
|
|
|
42
|
-
def cancel(order_number)
|
|
43
|
-
@http.post("#{BASE}/cancelOrderByOrderNumber", body: { orderNumber: order_number })
|
|
44
|
-
end
|
|
45
|
-
|
|
46
38
|
def get_items(order_number)
|
|
47
39
|
@http.get("#{BASE}/getAllOrderItemsByOrderNumber", query: { orderNumber: order_number })
|
|
48
40
|
end
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# Copyright (c) 2021-2026 by FlexOps, LLC. All rights reserved.
|
|
7
7
|
# ***********************************************************************
|
|
8
8
|
|
|
9
|
+
require "uri"
|
|
10
|
+
|
|
9
11
|
module FlexOps
|
|
10
12
|
module Resources
|
|
11
13
|
class Pickups
|
|
@@ -14,20 +16,27 @@ module FlexOps
|
|
|
14
16
|
@ws_id = ws_id_proc
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# request:: { carrierCode:, pickupDate:, street:, city:, state:, postalCode:,
|
|
20
|
+
# country: (optional, "US"), packageCount:, totalWeightOz: (optional),
|
|
21
|
+
# specialInstructions:, readyTime:, closeTime:, contactName:, contactPhone: (all optional) }
|
|
17
22
|
def schedule(request)
|
|
18
23
|
@http.post("#{ws_path}/pickups", body: request)
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
# Lists the workspace's scheduled pickups.
|
|
21
27
|
def list
|
|
22
|
-
@http.get("#{ws_path}/pickups")
|
|
28
|
+
@http.get("#{ws_path}/pickups/scheduled")
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
# The Gateway uses "usps" when carrier_code is left out.
|
|
32
|
+
def get(pickup_id, carrier_code: nil)
|
|
33
|
+
@http.get("#{ws_path}/pickups/#{pickup_id}", query: carrier_code.nil? ? nil : { carrierCode: carrier_code })
|
|
27
34
|
end
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
# The Gateway uses "usps" when carrier_code is left out.
|
|
37
|
+
def cancel(pickup_id, carrier_code: nil)
|
|
38
|
+
query = carrier_code.nil? ? "" : "?#{URI.encode_www_form(carrierCode: carrier_code)}"
|
|
39
|
+
@http.delete("#{ws_path}/pickups/#{pickup_id}#{query}")
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
private
|
|
@@ -1,58 +1,73 @@
|
|
|
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
|
+
# Recurring shipment schedules. Answers are bare JSON (no envelope); ids are GUID strings.
|
|
12
|
+
#
|
|
13
|
+
# Reads work with any API key or a user token. Writes (create, update, delete, pause, resume,
|
|
14
|
+
# trigger) need a user token (auth.login) or an API key granted the opt-in
|
|
15
|
+
# "recurring-shipments:write" scope; a key without it gets 403 API_KEY_SCOPE_REQUIRED.
|
|
16
|
+
#
|
|
17
|
+
# +interval+ is the Gateway RecurrenceInterval number: Daily 0, Weekly 1, Biweekly 2,
|
|
18
|
+
# Monthly 3, Custom 4 (Custom runs on +cronExpression+).
|
|
13
19
|
class RecurringShipments
|
|
14
20
|
def initialize(http, ws_id_proc)
|
|
15
21
|
@http = http
|
|
16
22
|
@ws_id = ws_id_proc
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
def list
|
|
20
|
-
|
|
21
|
-
@http.get("#{ws_path}/recurring-shipments/", query: query.empty? ? nil : query)
|
|
25
|
+
def list
|
|
26
|
+
@http.get(ws_path)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def get(id)
|
|
25
|
-
@http.get("#{ws_path}
|
|
30
|
+
@http.get("#{ws_path}/#{id}")
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
# request:: { name:, firstRunUtc: (ISO 8601), interval: (0-4), presetId:, addressId:,
|
|
34
|
+
# cronExpression:, maxFailures: (1-100, default 3) }. Each run needs a destination
|
|
35
|
+
# +addressId+; a +presetId+ supplies the package and service. Returns the schedule.
|
|
28
36
|
def create(request)
|
|
29
|
-
@http.post(
|
|
37
|
+
@http.post(ws_path, body: request)
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# request:: any of name:, presetId:, addressId:, interval:, cronExpression:, maxFailures:.
|
|
32
41
|
def update(id, request)
|
|
33
|
-
@http.put("#{ws_path}
|
|
42
|
+
@http.put("#{ws_path}/#{id}", body: request)
|
|
34
43
|
end
|
|
35
44
|
|
|
45
|
+
# Returns nil (204).
|
|
36
46
|
def delete(id)
|
|
37
|
-
@http.delete("#{ws_path}
|
|
47
|
+
@http.delete("#{ws_path}/#{id}")
|
|
38
48
|
end
|
|
39
49
|
|
|
50
|
+
# Returns nil (204).
|
|
40
51
|
def pause(id)
|
|
41
|
-
@http.post("#{ws_path}
|
|
52
|
+
@http.post("#{ws_path}/#{id}/pause")
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
# Returns nil (204).
|
|
44
56
|
def resume(id)
|
|
45
|
-
@http.post("#{ws_path}
|
|
57
|
+
@http.post("#{ws_path}/#{id}/resume")
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
# Runs the schedule now, which creates an order. Pass the same +idempotency_key+ when you
|
|
61
|
+
# retry a trigger yourself so the Gateway does not create a second order. Returns nil (204).
|
|
62
|
+
def trigger(id, idempotency_key: nil)
|
|
63
|
+
@http.post("#{ws_path}/#{id}/trigger",
|
|
64
|
+
headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
|
|
50
65
|
end
|
|
51
66
|
|
|
52
67
|
private
|
|
53
68
|
|
|
54
69
|
def ws_path
|
|
55
|
-
"/api/workspaces/#{@ws_id.call}"
|
|
70
|
+
"/api/workspaces/#{@ws_id.call}/recurring-shipments"
|
|
56
71
|
end
|
|
57
72
|
end
|
|
58
73
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
# Scheduled report emails. Answers use the { success, data, message } envelope, except
|
|
12
|
+
# delete, which returns nil (204). Ids are GUID strings.
|
|
13
|
+
#
|
|
14
|
+
# Reads work with any API key or a user token. Writes (create, update, delete, toggle) need a
|
|
15
|
+
# user token (auth.login) or an API key granted the opt-in "report-schedules:write" scope; a
|
|
16
|
+
# key without it gets 403 API_KEY_SCOPE_REQUIRED.
|
|
17
|
+
#
|
|
18
|
+
# Numbers on the wire: +format+ Csv 0, Json 1; +frequencyType+ Daily 0, Weekly 1, Monthly 2;
|
|
19
|
+
# +dayOfWeek+ 0-6 (Sunday 0, for Weekly); +dayOfMonth+ 1-28 (for Monthly).
|
|
20
|
+
class ReportSchedules
|
|
21
|
+
def initialize(http, ws_id_proc)
|
|
22
|
+
@http = http
|
|
23
|
+
@ws_id = ws_id_proc
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list
|
|
27
|
+
@http.get(ws_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(id)
|
|
31
|
+
@http.get("#{ws_path}/#{id}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# request:: { reportType: (e.g. "shipments"), format:, frequencyType:, timeOfDayUtc: ("HH:mm:ss"),
|
|
35
|
+
# recipientEmails: (comma-separated), dayOfWeek:, dayOfMonth:, filterParametersJson:,
|
|
36
|
+
# maxFailures: (1-100, default 3) }.
|
|
37
|
+
def create(request)
|
|
38
|
+
@http.post(ws_path, body: request)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# request:: any of the create fields.
|
|
42
|
+
def update(id, request)
|
|
43
|
+
@http.put("#{ws_path}/#{id}", body: request)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns nil (204).
|
|
47
|
+
def delete(id)
|
|
48
|
+
@http.delete("#{ws_path}/#{id}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Flips isEnabled. The envelope's data is nil; read the schedule to see the new state.
|
|
52
|
+
def toggle(id)
|
|
53
|
+
@http.post("#{ws_path}/#{id}/toggle")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def ws_path
|
|
59
|
+
"/api/workspaces/#{@ws_id.call}/report-schedules"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -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,41 +16,70 @@ module FlexOps
|
|
|
14
16
|
@ws_id = ws_id_proc
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
|
|
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.
|
|
21
|
+
def list(page: nil, page_size: nil, start_date: nil, end_date: nil)
|
|
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
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def approve(return_id)
|
|
31
|
-
@http.post("#{ws_path}/returns/#{return_id}/approve")
|
|
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)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
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)}")
|
|
36
41
|
end
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
# Creates an RMA and returns the enveloped RMA (201).
|
|
44
|
+
#
|
|
45
|
+
# request:: { orderId: (a number), orderNumber: (optional), reason:,
|
|
46
|
+
# items: [{ sku:, quantity:, reason: (optional) }], notes: (optional),
|
|
47
|
+
# customerEmail: (optional), reasonId: (optional) }
|
|
48
|
+
def create(request)
|
|
49
|
+
@http.post("#{ws_path}/returns/rma", body: request)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
# Generates a return label for an RMA.
|
|
53
|
+
#
|
|
54
|
+
# The carrier needs the package weight and all three dimensions: pass weight_oz, length_in,
|
|
55
|
+
# width_in and height_in. Anything left out comes from the workspace's default shipping
|
|
56
|
+
# preset; with no preset the Gateway answers 400 PackageMeasurementsRequired naming the
|
|
57
|
+
# missing fields.
|
|
58
|
+
#
|
|
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.
|
|
61
|
+
def generate_label(rma_number, weight_oz: nil, length_in: nil, width_in: nil, height_in: nil,
|
|
62
|
+
carrier_code: nil, service_type: nil, payment_confirmed: nil, hazmat_classification: nil,
|
|
63
|
+
rate_indicator: nil)
|
|
64
|
+
# Always a JSON object: the Gateway binds a required GenerateReturnLabelRequest body and refuses an empty one with 400.
|
|
65
|
+
body = {
|
|
66
|
+
carrierCode: carrier_code, serviceType: service_type, weightOz: weight_oz, lengthIn: length_in,
|
|
67
|
+
widthIn: width_in, heightIn: height_in, paymentConfirmed: payment_confirmed,
|
|
68
|
+
hazmatClassification: hazmat_classification, rateIndicator: rate_indicator
|
|
69
|
+
}.compact
|
|
70
|
+
@http.post("#{ws_path}/returns/#{rma_number}/label", body: body)
|
|
48
71
|
end
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
# Records the inspected items of a received RMA.
|
|
74
|
+
#
|
|
75
|
+
# rma_id:: the numeric RMA id (the route segment)
|
|
76
|
+
# rma_number:: the RMA number, for example "RMA-1001"
|
|
77
|
+
# inspected_items:: [{ partNumber:, partDescription: (optional), quantity:,
|
|
78
|
+
# disposition: 0 restock, 1 dispose or 2 forward, inspectionNotes: (optional) }]
|
|
79
|
+
# warehouse_id:: the receiving warehouse; the Gateway uses 1 when it is left out
|
|
80
|
+
def mark_received(rma_id, rma_number:, inspected_items:, warehouse_id: nil)
|
|
81
|
+
body = { rmaNumber: rma_number, warehouseId: warehouse_id, inspectedItems: inspected_items }.compact
|
|
82
|
+
@http.post("#{ws_path}/returns/#{rma_id}/receive", body: body)
|
|
52
83
|
end
|
|
53
84
|
|
|
54
85
|
private
|
|
@@ -22,10 +22,13 @@ module FlexOps
|
|
|
22
22
|
@http.get("#{ws_path}/shipping-rules/#{rule_id}")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# rule:: { name:, priority:, isEnabled:, warehouseId: (optional),
|
|
26
|
+
# conditions: [{ field:, operator:, value:, group: (optional) }], actions: [{ type:, value: }] }
|
|
25
27
|
def create(rule)
|
|
26
28
|
@http.post("#{ws_path}/shipping-rules", body: rule)
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# rule:: the fields to change, named as for #create.
|
|
29
32
|
def update(rule_id, rule)
|
|
30
33
|
@http.put("#{ws_path}/shipping-rules/#{rule_id}", body: rule)
|
|
31
34
|
end
|
|
@@ -34,8 +37,21 @@ module FlexOps
|
|
|
34
37
|
@http.delete("#{ws_path}/shipping-rules/#{rule_id}")
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
# Sets rule priorities. +rules+ is an array of { ruleId:, priority: } hashes, or of rule ids
|
|
41
|
+
# in the order to apply (the first gets priority 0).
|
|
42
|
+
def reorder(rules)
|
|
43
|
+
items = rules.each_with_index.map do |rule, index|
|
|
44
|
+
rule.is_a?(Hash) ? rule : { ruleId: rule, priority: index }
|
|
45
|
+
end
|
|
46
|
+
@http.post("#{ws_path}/shipping-rules/reorder", body: { rules: items })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Evaluates the workspace's rules against a sample shipment without changing anything.
|
|
50
|
+
#
|
|
51
|
+
# request:: { weightOz:, declaredValue:, destinationCountry:, destinationState:,
|
|
52
|
+
# destinationPostalCode:, orderSource:, itemCount:, shipMethod:, destinationZone:, tags: }
|
|
53
|
+
def test(request)
|
|
54
|
+
@http.post("#{ws_path}/shipping-rules/test", body: request)
|
|
39
55
|
end
|
|
40
56
|
|
|
41
57
|
private
|
|
@@ -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 Shipping
|
|
@@ -27,24 +29,63 @@ module FlexOps
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Returns a raw preview or label; confirmation always belongs to the caller.
|
|
32
|
+
# A label without an orderId must declare hazmatClassification (a HazmatClassification;
|
|
33
|
+
# HazmatClassification::NONE for an ordinary parcel). The SDK never fills it in.
|
|
30
34
|
def create_label(request = {}, idempotency_key: nil, **fields)
|
|
31
35
|
request = request.merge(fields)
|
|
32
|
-
@http.post("
|
|
36
|
+
@http.post("/api/shipping/labels", body: request,
|
|
33
37
|
headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
# The Gateway requires the carrier code (for example "USPS"). It is a keyword with no
|
|
41
|
+
# default value so older call sites fail with a clear ArgumentError before any request.
|
|
42
|
+
def cancel_label(label_id, carrier_code: nil)
|
|
43
|
+
raise ArgumentError, "carrier_code is required to cancel a label (for example \"USPS\")" if carrier_code.to_s.empty?
|
|
44
|
+
|
|
45
|
+
@http.delete("/api/shipping/labels/#{escape(label_id)}?carrierCode=#{escape(carrier_code)}")
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def track(tracking_number)
|
|
41
|
-
@http.get("
|
|
49
|
+
@http.get("/api/shipping/track/#{escape(tracking_number)}")
|
|
42
50
|
end
|
|
43
51
|
|
|
52
|
+
# Validates an address with POST /api/addresses/validate and returns the bare result
|
|
53
|
+
# ("isValid", "validationUnavailable", "suggestedAddress", "isResidential", "warnings",
|
|
54
|
+
# "validatedBy", "checkedAt").
|
|
55
|
+
#
|
|
56
|
+
# The address may use the rate and label field names (addressLine1, stateProvince,
|
|
57
|
+
# countryCode, ...), the legacy street1/state/zip names, or the Gateway's own
|
|
58
|
+
# (line1, line2, city, state, postalCode, country). Only those fields and name are sent.
|
|
44
59
|
def validate_address(address)
|
|
45
|
-
@http.post("
|
|
60
|
+
@http.post("/api/addresses/validate", body: to_validation_address(address))
|
|
61
|
+
end
|
|
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),
|
|
68
|
+
# packages: [{ weightInOunces:, lengthInInches:, widthInInches:, heightInInches:,
|
|
69
|
+
# deliveryConfirmationRequired: (optional), signatureConfirmationRequired: (optional),
|
|
70
|
+
# insuranceCoverageRequested: (optional), insuredValue: (optional),
|
|
71
|
+
# packageReference: (optional) }] }
|
|
72
|
+
#
|
|
73
|
+
# Weight is in ounces and every dimension in inches; all four are required. Multi-package
|
|
74
|
+
# labels cannot carry HAZMAT yet: send HazmatClassification::NONE or leave hazmatClassification
|
|
75
|
+
# out (any other class is refused with 400 HazmatNotSupported).
|
|
76
|
+
#
|
|
77
|
+
# Returns the envelope when at least one label was bought (check data["failedPackages"] and
|
|
78
|
+
# each box in data["labels"]). When none was, the Gateway answers 422 AllPackagesFailed, or 409
|
|
79
|
+
# OutcomeUnknown when a box's carrier outcome is unknown and a label may exist: both raise
|
|
80
|
+
# FlexOps::Error whose data holds the per-box results. After OutcomeUnknown, check the order
|
|
81
|
+
# before buying again. Pass your own idempotency_key (and reuse it) to make a retry you run
|
|
82
|
+
# yourself safe.
|
|
83
|
+
def create_multi_package(request, idempotency_key: nil)
|
|
84
|
+
@http.post("/api/shipping/multi-package", body: request,
|
|
85
|
+
headers: idempotency_key.nil? ? {} : { "Idempotency-Key" => idempotency_key })
|
|
46
86
|
end
|
|
47
87
|
|
|
88
|
+
# The payload is sent as given. Set hazmatClassification on an item only when you have a declaration.
|
|
48
89
|
def create_batch(request)
|
|
49
90
|
@http.post("#{ws_path}/labels/batch", body: request)
|
|
50
91
|
end
|
|
@@ -62,19 +103,19 @@ module FlexOps
|
|
|
62
103
|
end
|
|
63
104
|
|
|
64
105
|
def get_carriers
|
|
65
|
-
@http.get("
|
|
106
|
+
@http.get("/api/shipping/carriers")
|
|
66
107
|
end
|
|
67
108
|
|
|
68
109
|
def get_recommendations(request)
|
|
69
|
-
@http.post("
|
|
110
|
+
@http.post("/api/shipping/recommendations", body: request)
|
|
70
111
|
end
|
|
71
112
|
|
|
72
113
|
def predict_delivery(request)
|
|
73
|
-
@http.post("
|
|
114
|
+
@http.post("/api/shipping/predictions/delivery", body: request)
|
|
74
115
|
end
|
|
75
116
|
|
|
76
117
|
def get_savings
|
|
77
|
-
@http.get("
|
|
118
|
+
@http.get("/api/shipping/savings")
|
|
78
119
|
end
|
|
79
120
|
|
|
80
121
|
private
|
|
@@ -82,6 +123,28 @@ module FlexOps
|
|
|
82
123
|
def ws_path
|
|
83
124
|
"/api/workspaces/#{@ws_id.call}"
|
|
84
125
|
end
|
|
126
|
+
|
|
127
|
+
def escape(segment)
|
|
128
|
+
ERB::Util.url_encode(segment.to_s)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Gateway AddressValidationRequest field for each accepted address field name.
|
|
132
|
+
VALIDATION_ADDRESS_FIELDS = {
|
|
133
|
+
"name" => "name",
|
|
134
|
+
"line1" => "line1", "addressLine1" => "line1", "street1" => "line1",
|
|
135
|
+
"line2" => "line2", "addressLine2" => "line2", "street2" => "line2",
|
|
136
|
+
"city" => "city",
|
|
137
|
+
"state" => "state", "stateProvince" => "state",
|
|
138
|
+
"postalCode" => "postalCode", "zip" => "postalCode",
|
|
139
|
+
"country" => "country", "countryCode" => "country"
|
|
140
|
+
}.freeze
|
|
141
|
+
|
|
142
|
+
def to_validation_address(address)
|
|
143
|
+
address.each_with_object({}) do |(key, value), mapped|
|
|
144
|
+
field = VALIDATION_ADDRESS_FIELDS[key.to_s]
|
|
145
|
+
mapped[field] = value unless field.nil? || value.nil?
|
|
146
|
+
end
|
|
147
|
+
end
|
|
85
148
|
end
|
|
86
149
|
end
|
|
87
150
|
end
|
|
@@ -14,27 +14,11 @@ module FlexOps
|
|
|
14
14
|
@ws_id = ws_id_proc
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Returns the configured workspace's balance, bare: { "hasWallet", "balanceCents", "balance" }.
|
|
18
|
+
# Funding and transaction history are managed in the FlexOps portal.
|
|
17
19
|
def get_balance
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def add_funds(amount)
|
|
22
|
-
@http.post("#{ws_path}/wallet/add-funds", body: { amount: amount })
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def list_transactions(page: nil, page_size: nil, start_date: nil, end_date: nil)
|
|
26
|
-
query = { page: page, pageSize: page_size, startDate: start_date, endDate: end_date }.compact
|
|
27
|
-
@http.get("#{ws_path}/wallet/transactions", query: query.empty? ? nil : query)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def configure_auto_reload(enabled:, threshold: nil, amount: nil)
|
|
31
|
-
@http.put("#{ws_path}/wallet/auto-reload", body: { enabled: enabled, threshold: threshold, amount: amount }.compact)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def ws_path
|
|
37
|
-
"/api/workspaces/#{@ws_id.call}"
|
|
20
|
+
ws_id = @ws_id.call
|
|
21
|
+
@http.get("/api/wallet/my-balance", headers: ws_id.to_s.empty? ? {} : { "X-Workspace-Id" => ws_id.to_s })
|
|
38
22
|
end
|
|
39
23
|
end
|
|
40
24
|
end
|