fastbound 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8219bcb38d03ea269bb7d9c4a16a7e062b6beb001f88f6763eecac18cec9a66
4
+ data.tar.gz: 01d7d0f70d77527d5e9d5fc12445493780ba10f539471360c84522820c164ddc
5
+ SHA512:
6
+ metadata.gz: 3fe98da320c0330c51c7e51b36e6e98feb395174988033d1c067f60ddcff8a4f6f8da4d1b9024fe6c52f80ce4e6fafa520cf3722e75ed5b9b5b82b8f60af59a8
7
+ data.tar.gz: f8db9b2ef652c56350a71929751b1d06cfd69c259467fe54755c8ad52c1cf7f9f182aa8035eea73d749003b94b6f2a0b8303fe447d943602bb6a51af429f635a
data/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # Fastbound
2
+
3
+ A Ruby gem for the [FastBound](https://fastbound.com) firearms compliance API.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "fastbound"
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```bash
16
+ gem install fastbound
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ```ruby
22
+ client = Fastbound::Client.new(
23
+ api_key: "your_api_key",
24
+ account_number: "A001234",
25
+ audit_user: "user@example.com" # required for create/update/delete operations
26
+ )
27
+ ```
28
+
29
+ | Option | Required | Description |
30
+ |---|---|---|
31
+ | `api_key` | Yes | Your FastBound API key (used as HTTP Basic auth username) |
32
+ | `account_number` | Yes | Your FastBound account number |
33
+ | `audit_user` | No* | Email address recorded on write operations. Required by the API for POST/PUT/DELETE. |
34
+ | `base_url` | No | Override the API base URL (default: `https://cloud.fastbound.com`) |
35
+
36
+ ## Resources
37
+
38
+ ### Account
39
+
40
+ ```ruby
41
+ client.account.get
42
+ ```
43
+
44
+ ### Acquisitions
45
+
46
+ ```ruby
47
+ # List acquisitions
48
+ client.acquisitions.list(take: 25, skip: 0)
49
+ client.acquisitions.list(type: "Purchase", acquired_from_contact_id: "uuid")
50
+
51
+ # Find by ID or external ID
52
+ client.acquisitions.find("uuid")
53
+ client.acquisitions.find_by_external_id("my-ext-id")
54
+
55
+ # Create
56
+ client.acquisitions.create(
57
+ date: "2024-01-15",
58
+ type: "Purchase",
59
+ invoice_number: "INV-001",
60
+ external_id: "my-acq-001"
61
+ )
62
+
63
+ # Update
64
+ client.acquisitions.update("uuid", invoice_number: "INV-002")
65
+
66
+ # Delete
67
+ client.acquisitions.destroy("uuid")
68
+
69
+ # Attach a contact
70
+ client.acquisitions.attach_contact("acquisition-uuid", "contact-uuid")
71
+
72
+ # Commit
73
+ client.acquisitions.commit("uuid")
74
+ client.acquisitions.commit("uuid", list_acquired_items: true)
75
+
76
+ # Create and commit in one request
77
+ client.acquisitions.create_and_commit(
78
+ date: "2024-01-15",
79
+ type: "Purchase",
80
+ contact_id: "contact-uuid",
81
+ items: [{ manufacturer: "Glock", model: "19", serial: "ABC123", type: "Pistol", caliber: "9mm" }]
82
+ )
83
+
84
+ # Create as pending (does not commit)
85
+ client.acquisitions.create_as_pending(
86
+ date: "2024-01-15",
87
+ type: "Purchase",
88
+ contact_id: "contact-uuid",
89
+ items: [{ manufacturer: "Glock", model: "19", serial: "ABC123", type: "Pistol", caliber: "9mm" }]
90
+ )
91
+
92
+ # Items
93
+ client.acquisitions.get_item("acquisition-uuid", "item-uuid")
94
+ client.acquisitions.add_item("acquisition-uuid", manufacturer: "Glock", model: "19", serial: "ABC123", type: "Pistol", caliber: "9mm")
95
+ client.acquisitions.add_items("acquisition-uuid", [
96
+ { manufacturer: "Glock", model: "19", serial: "ABC123", type: "Pistol", caliber: "9mm" },
97
+ { manufacturer: "Glock", model: "17", serial: "DEF456", type: "Pistol", caliber: "9mm" }
98
+ ])
99
+ client.acquisitions.update_item("acquisition-uuid", "item-uuid", location: "Safe A")
100
+ client.acquisitions.delete_item("acquisition-uuid", "item-uuid")
101
+ ```
102
+
103
+ ### Attachments
104
+
105
+ ```ruby
106
+ # Returns raw binary data
107
+ pdf_bytes = client.attachments.download("attachment-uuid")
108
+ File.binwrite("attachment.pdf", pdf_bytes)
109
+ ```
110
+
111
+ ### Contacts
112
+
113
+ ```ruby
114
+ # List contacts
115
+ client.contacts.list(take: 25, skip: 0)
116
+ client.contacts.list(last_name: "Smith", ffl_number: "1-23-456-07-8A-12345")
117
+
118
+ # Find by ID or external ID
119
+ client.contacts.find("uuid")
120
+ client.contacts.find_by_external_id("my-ext-id")
121
+
122
+ # Create
123
+ client.contacts.create(
124
+ first_name: "John",
125
+ last_name: "Smith",
126
+ ffl_number: "1-23-456-07-8A-12345",
127
+ ffl_expires: "2026-12-31",
128
+ premise_address1: "123 Main St",
129
+ premise_city: "Anytown",
130
+ premise_state: "TX",
131
+ premise_zip_code: "75001"
132
+ )
133
+
134
+ # Update
135
+ client.contacts.update("uuid", email_address: "john@example.com")
136
+
137
+ # Delete
138
+ client.contacts.destroy("uuid")
139
+
140
+ # Merge two contacts (winning contact receives all data from losing contact)
141
+ client.contacts.merge(winning_contact_id: "uuid-1", losing_contact_id: "uuid-2")
142
+
143
+ # Licenses
144
+ client.contacts.get_license("contact-uuid", "license-uuid")
145
+ client.contacts.create_license("contact-uuid", type: "FFL", number: "1-23-456-07-8A-12345", expiration: "2026-12-31")
146
+ client.contacts.update_license("contact-uuid", "license-uuid", copy_on_file: true)
147
+ client.contacts.delete_license("contact-uuid", "license-uuid")
148
+ ```
149
+
150
+ ### Dispositions
151
+
152
+ ```ruby
153
+ # List dispositions
154
+ client.dispositions.list(take: 25, skip: 0)
155
+ client.dispositions.list(type: "SaleTo", disposed_to_contact_id: "uuid")
156
+ client.dispositions.list(include_4473: true)
157
+
158
+ # List Form 4473 dispositions
159
+ client.dispositions.list_4473s(take: 25)
160
+ client.dispositions.list_4473s(include_awaiting_completion: true)
161
+
162
+ # Find by ID or external ID
163
+ client.dispositions.find("uuid")
164
+ client.dispositions.find_by_external_id("my-ext-id")
165
+
166
+ # Create standard disposition
167
+ client.dispositions.create(type: "SaleTo", date: "2024-01-15", generate_ttsn: true)
168
+
169
+ # Create special disposition types
170
+ client.dispositions.create_nfa(date: "2024-01-15", type: "SaleTo", submission_date: "2024-01-10")
171
+ client.dispositions.create_theft_loss(date: "2024-01-15", theft_loss__type: "Theft")
172
+ client.dispositions.create_destroyed(date: "2024-01-15", destroyed__description: "Destroyed per ATF")
173
+
174
+ # Update
175
+ client.dispositions.update("uuid", note: "Updated note")
176
+
177
+ # Delete
178
+ client.dispositions.destroy("uuid")
179
+
180
+ # Attach a contact
181
+ client.dispositions.attach_contact("disposition-uuid", "contact-uuid")
182
+
183
+ # Lock
184
+ client.dispositions.lock("uuid")
185
+ client.dispositions.lock_by_external_id("my-ext-id")
186
+
187
+ # Commit
188
+ client.dispositions.commit("uuid")
189
+ client.dispositions.commit("uuid", list_disposed_items: true)
190
+
191
+ # Create and commit in one request
192
+ client.dispositions.create_and_commit(
193
+ type: "SaleTo",
194
+ date: "2024-01-15",
195
+ contact_id: "contact-uuid",
196
+ items: [{ id: "item-uuid", price: 500.00 }]
197
+ )
198
+
199
+ # Create as pending (does not commit)
200
+ client.dispositions.create_as_pending(
201
+ type: "SaleTo",
202
+ date: "2024-01-15",
203
+ contact_id: "contact-uuid",
204
+ items: [{ id: "item-uuid", price: 500.00 }]
205
+ )
206
+
207
+ # Items
208
+ client.dispositions.list_items("disposition-uuid")
209
+ client.dispositions.add_items("disposition-uuid", [{ id: "item-uuid", price: 500.00 }])
210
+ client.dispositions.add_items_by_external_id("disp-ext-id", [{ external_id: "item-ext-id", price: 500.00 }])
211
+ client.dispositions.add_items_by_search("disp-ext-id", manufacturer: "Glock", model: "19", caliber: "9mm")
212
+ client.dispositions.edit_item_price("disposition-uuid", "item-uuid", price: 550.00)
213
+ client.dispositions.remove_item("disposition-uuid", "item-uuid")
214
+ client.dispositions.remove_item_by_external_id("disposition-uuid", "item-ext-id")
215
+ ```
216
+
217
+ ### Downloads
218
+
219
+ ```ruby
220
+ # Download bound book as binary (PDF or CSV depending on API response)
221
+ data = client.downloads.bound_book
222
+ File.binwrite("bound_book.pdf", data)
223
+ ```
224
+
225
+ ### Form 4473s
226
+
227
+ ```ruby
228
+ # Returns raw binary PDF data
229
+ pdf_bytes = client.form4473s.download("form4473-uuid")
230
+ File.binwrite("form4473.pdf", pdf_bytes)
231
+ ```
232
+
233
+ ### Inventory
234
+
235
+ ```ruby
236
+ # Bulk verify inventory by serial number
237
+ result = client.inventory.bulk_verify(
238
+ serials: ["ABC123", "DEF456", "GHI789"],
239
+ rollback_partial: true,
240
+ update_location: true,
241
+ location: "Safe A",
242
+ verified_utc: "2024-01-15T10:00:00Z"
243
+ )
244
+ ```
245
+
246
+ ### Items
247
+
248
+ ```ruby
249
+ # List items (extensive filtering available)
250
+ client.items.list(take: 25, skip: 0)
251
+ client.items.list(manufacturer: "Glock", caliber: "9mm", status: 1)
252
+ client.items.list(serial: "ABC123")
253
+ client.items.list(acquired_on_or_after: "2024-01-01", acquired_on_or_before: "2024-12-31")
254
+ client.items.list(is_theft_loss: false, is_destroyed: false)
255
+
256
+ # Find by ID or external ID
257
+ client.items.find("uuid")
258
+ client.items.find_by_external_id("my-ext-id")
259
+
260
+ # Update
261
+ client.items.update("uuid", location: "Safe B", note: "Cleaned and inspected")
262
+
263
+ # Delete (logical delete with type)
264
+ client.items.delete("uuid", delete_type: "Destroyed", delete_note: "Per ATF guidelines")
265
+
266
+ # Set acquisition contact
267
+ client.items.set_acquisition_contact("item-uuid", "contact-uuid")
268
+
269
+ # Undispose
270
+ client.items.undispose("uuid")
271
+ client.items.undispose("uuid", note: "Returned by customer")
272
+
273
+ # Set external IDs
274
+ client.items.set_external_id("uuid", external_id: "my-item-001")
275
+ client.items.set_external_ids([
276
+ { id: "uuid-1", external_id: "my-item-001" },
277
+ { id: "uuid-2", external_id: "my-item-002" }
278
+ ])
279
+ ```
280
+
281
+ ### Multiple Sale Reports
282
+
283
+ ```ruby
284
+ # Returns raw binary PDF data
285
+ pdf_bytes = client.multiple_sale_reports.download("report-uuid", "attachment-uuid")
286
+ File.binwrite("multiple_sale_report.pdf", pdf_bytes)
287
+ ```
288
+
289
+ ### Smart Lists
290
+
291
+ Smart lists return the allowed values for various fields in your account.
292
+
293
+ ```ruby
294
+ client.smart_lists.acquire_types
295
+ client.smart_lists.calibers
296
+ client.smart_lists.conditions
297
+ client.smart_lists.countries_of_manufacture
298
+ client.smart_lists.delete_types
299
+ client.smart_lists.dispose_types
300
+ client.smart_lists.importers
301
+ client.smart_lists.item_types
302
+ client.smart_lists.license_types
303
+ client.smart_lists.locations
304
+ client.smart_lists.manufacturers
305
+ client.smart_lists.theft_loss_types
306
+ client.smart_lists.manufacturing_dispose_types
307
+ client.smart_lists.manufacturing_acquire_types
308
+ ```
309
+
310
+ ### Users
311
+
312
+ ```ruby
313
+ client.users.list
314
+ client.users.list(include_disabled: true)
315
+ ```
316
+
317
+ ### Webhooks
318
+
319
+ ```ruby
320
+ # List available webhook events
321
+ client.webhooks.list_events
322
+
323
+ # Find a webhook by name
324
+ client.webhooks.find("my-webhook")
325
+
326
+ # Create a webhook
327
+ client.webhooks.create(
328
+ name: "my-webhook",
329
+ url: "https://example.com/webhooks/fastbound",
330
+ description: "Acquisition notifications",
331
+ events: ["acquisition.committed", "disposition.committed"]
332
+ )
333
+
334
+ # Update a webhook
335
+ client.webhooks.update("my-webhook", url: "https://example.com/webhooks/v2")
336
+
337
+ # Delete a webhook
338
+ client.webhooks.destroy("my-webhook")
339
+ ```
340
+
341
+ ## Error Handling
342
+
343
+ ```ruby
344
+ begin
345
+ client.contacts.find("nonexistent-uuid")
346
+ rescue Fastbound::NotFoundError => e
347
+ puts "Not found: #{e.message}"
348
+ rescue Fastbound::UnprocessableEntityError => e
349
+ puts "Validation failed: #{e.errors.join(", ")}"
350
+ rescue Fastbound::UnauthorizedError => e
351
+ puts "Auth error: #{e.message}"
352
+ rescue Fastbound::ApiError => e
353
+ puts "API error #{e.status}: #{e.message}"
354
+ end
355
+ ```
356
+
357
+ | Exception | HTTP Status |
358
+ |---|---|
359
+ | `Fastbound::UnauthorizedError` | 401, 403 |
360
+ | `Fastbound::NotFoundError` | 404 |
361
+ | `Fastbound::UnprocessableEntityError` | 400, 422 |
362
+ | `Fastbound::ApiError` | All other errors |
363
+
364
+ ## License
365
+
366
+ MIT
@@ -0,0 +1,154 @@
1
+ require "faraday"
2
+ require "json"
3
+
4
+ module Fastbound
5
+ class Client
6
+ BASE_URL = "https://cloud.fastbound.com"
7
+
8
+ attr_reader :account_number, :audit_user
9
+
10
+ def initialize(api_key:, account_number:, audit_user: nil, base_url: BASE_URL)
11
+ raise ConfigurationError, "api_key is required" if api_key.nil? || api_key.empty?
12
+ raise ConfigurationError, "account_number is required" if account_number.nil? || account_number.empty?
13
+
14
+ @api_key = api_key
15
+ @account_number = account_number
16
+ @audit_user = audit_user
17
+ @base_url = base_url
18
+ end
19
+
20
+ def account
21
+ @account ||= Resources::Account.new(self)
22
+ end
23
+
24
+ def acquisitions
25
+ @acquisitions ||= Resources::Acquisitions.new(self)
26
+ end
27
+
28
+ def attachments
29
+ @attachments ||= Resources::Attachments.new(self)
30
+ end
31
+
32
+ def contacts
33
+ @contacts ||= Resources::Contacts.new(self)
34
+ end
35
+
36
+ def dispositions
37
+ @dispositions ||= Resources::Dispositions.new(self)
38
+ end
39
+
40
+ def downloads
41
+ @downloads ||= Resources::Downloads.new(self)
42
+ end
43
+
44
+ def form4473s
45
+ @form4473s ||= Resources::Form4473s.new(self)
46
+ end
47
+
48
+ def inventory
49
+ @inventory ||= Resources::Inventory.new(self)
50
+ end
51
+
52
+ def items
53
+ @items ||= Resources::Items.new(self)
54
+ end
55
+
56
+ def multiple_sale_reports
57
+ @multiple_sale_reports ||= Resources::MultipleSaleReports.new(self)
58
+ end
59
+
60
+ def smart_lists
61
+ @smart_lists ||= Resources::SmartLists.new(self)
62
+ end
63
+
64
+ def users
65
+ @users ||= Resources::Users.new(self)
66
+ end
67
+
68
+ def webhooks
69
+ @webhooks ||= Resources::Webhooks.new(self)
70
+ end
71
+
72
+ def get(path, params = {})
73
+ request(:get, path, params: compact(params))
74
+ end
75
+
76
+ def post(path, body = {}, audit: true)
77
+ request(:post, path, body: body, audit: audit)
78
+ end
79
+
80
+ def put(path, body = {}, audit: true)
81
+ request(:put, path, body: body, audit: audit)
82
+ end
83
+
84
+ def delete(path, audit: true)
85
+ request(:delete, path, audit: audit)
86
+ end
87
+
88
+ def get_binary(path)
89
+ response = connection.get(path)
90
+ handle_response(response, raw: true)
91
+ end
92
+
93
+ def post_binary(path, body = {}, audit: true)
94
+ response = connection(audit: audit).post(path) do |req|
95
+ req.body = body.to_json
96
+ req.headers["Content-Type"] = "application/json"
97
+ end
98
+ handle_response(response, raw: true)
99
+ end
100
+
101
+ def base_path
102
+ "/#{account_number}/api"
103
+ end
104
+
105
+ private
106
+
107
+ def request(method, path, params: nil, body: nil, audit: false)
108
+ response = connection(audit: audit).public_send(method, path) do |req|
109
+ req.params = params if params&.any?
110
+ if body
111
+ req.body = body.to_json
112
+ req.headers["Content-Type"] = "application/json"
113
+ end
114
+ end
115
+ handle_response(response)
116
+ end
117
+
118
+ def handle_response(response, raw: false)
119
+ case response.status
120
+ when 200, 201
121
+ return response.body if raw
122
+ response.body.empty? ? nil : JSON.parse(response.body)
123
+ when 204
124
+ nil
125
+ when 401, 403
126
+ raise UnauthorizedError.new(response.status, safe_parse(response.body))
127
+ when 404
128
+ raise NotFoundError.new(response.status, safe_parse(response.body))
129
+ when 400, 422
130
+ raise UnprocessableEntityError.new(response.status, safe_parse(response.body))
131
+ else
132
+ raise ApiError.new(response.status, safe_parse(response.body))
133
+ end
134
+ end
135
+
136
+ def safe_parse(body)
137
+ JSON.parse(body)
138
+ rescue StandardError
139
+ {}
140
+ end
141
+
142
+ def compact(hash)
143
+ hash.reject { |_, v| v.nil? }
144
+ end
145
+
146
+ def connection(audit: false)
147
+ Faraday.new(url: @base_url) do |conn|
148
+ conn.request :authorization, :basic, @api_key, ""
149
+ conn.headers["Accept"] = "application/json"
150
+ conn.headers["X-AuditUser"] = @audit_user if audit && @audit_user
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,31 @@
1
+ module Fastbound
2
+ class Error < StandardError; end
3
+ class ConfigurationError < Error; end
4
+
5
+ class ApiError < Error
6
+ attr_reader :status, :errors
7
+
8
+ def initialize(status, body)
9
+ @status = status
10
+ @errors = parse_errors(body)
11
+ super(build_message)
12
+ end
13
+
14
+ private
15
+
16
+ def parse_errors(body)
17
+ return [] unless body.is_a?(Hash)
18
+
19
+ Array(body["errors"]).map { |e| e.is_a?(Hash) ? e["message"] : e.to_s }.compact
20
+ end
21
+
22
+ def build_message
23
+ base = "HTTP #{status}"
24
+ errors.any? ? "#{base}: #{errors.join(", ")}" : base
25
+ end
26
+ end
27
+
28
+ class NotFoundError < ApiError; end
29
+ class UnprocessableEntityError < ApiError; end
30
+ class UnauthorizedError < ApiError; end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Account
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def get
9
+ @client.get("#{@client.base_path}/Account")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,97 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Acquisitions
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(take: nil, skip: nil, id: nil, external_id: nil, type: nil,
9
+ purchase_order_number: nil, invoice_number: nil,
10
+ shipment_tracking_number: nil, is_manufacturing_acquisition: nil,
11
+ acquired_from_contact_id: nil, acquired_from_contact_external_id: nil,
12
+ item_id: nil, item_external_id: nil)
13
+ @client.get("#{base}/Acquisitions", {
14
+ take: take, skip: skip, id: id, externalId: external_id, type: type,
15
+ purchaseOrderNumber: purchase_order_number, invoiceNumber: invoice_number,
16
+ shipmentTrackingNumber: shipment_tracking_number,
17
+ isManufacturingAcquisition: is_manufacturing_acquisition,
18
+ acquiredFromContactId: acquired_from_contact_id,
19
+ acquiredFromContactExternalId: acquired_from_contact_external_id,
20
+ itemId: item_id, itemExternalId: item_external_id
21
+ })
22
+ end
23
+
24
+ def find(id)
25
+ @client.get("#{base}/Acquisitions/#{id}")
26
+ end
27
+
28
+ def find_by_external_id(external_id)
29
+ @client.get("#{base}/Acquisitions/GetByExternalId/#{external_id}")
30
+ end
31
+
32
+ def create(params = {})
33
+ @client.post("#{base}/Acquisitions", params)
34
+ end
35
+
36
+ def update(id, params = {})
37
+ @client.put("#{base}/Acquisitions/#{id}", params)
38
+ end
39
+
40
+ def destroy(id)
41
+ @client.delete("#{base}/Acquisitions/#{id}")
42
+ end
43
+
44
+ def attach_contact(id, contact_id)
45
+ @client.put("#{base}/Acquisitions/#{id}/AttachContact/#{contact_id}", {})
46
+ end
47
+
48
+ def commit(id, params = {}, list_acquired_items: nil)
49
+ path = "#{base}/Acquisitions/#{id}/Commit"
50
+ path += "?listAcquiredItems=true" if list_acquired_items
51
+ @client.post(path, params)
52
+ end
53
+
54
+ def create_and_commit(params = {}, list_acquired_items: nil)
55
+ path = "#{base}/Acquisitions/CreateAndCommit"
56
+ path += "?listAcquiredItems=true" if list_acquired_items
57
+ @client.post(path, params)
58
+ end
59
+
60
+ def create_as_pending(params = {})
61
+ @client.post("#{base}/Acquisitions/CreateAsPending", params)
62
+ end
63
+
64
+ # Item operations
65
+
66
+ def get_item(id, acquisition_item_id)
67
+ @client.get("#{base}/Acquisitions/#{id}/Items/#{acquisition_item_id}")
68
+ end
69
+
70
+ def get_item_by_external_ids(acquisition_external_id, acquisition_item_external_id)
71
+ @client.get("#{base}/Acquisitions/#{acquisition_external_id}/Items/#{acquisition_item_external_id}")
72
+ end
73
+
74
+ def add_item(id, params = {})
75
+ @client.post("#{base}/Acquisitions/#{id}/Items", params)
76
+ end
77
+
78
+ def add_items(id, items = [])
79
+ @client.post("#{base}/Acquisitions/#{id}/Items/Multiple", { items: items })
80
+ end
81
+
82
+ def update_item(id, acquisition_item_id, params = {})
83
+ @client.put("#{base}/Acquisitions/#{id}/Items/#{acquisition_item_id}", params)
84
+ end
85
+
86
+ def delete_item(id, acquisition_item_id)
87
+ @client.delete("#{base}/Acquisitions/#{id}/Items/#{acquisition_item_id}")
88
+ end
89
+
90
+ private
91
+
92
+ def base
93
+ @client.base_path
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Attachments
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def download(attachment_id)
9
+ @client.get_binary("#{@client.base_path}/Attachments/Download/#{attachment_id}")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,71 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Contacts
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(license_name: nil, trade_name: nil, ffl_number: nil,
9
+ organization_name: nil, first_name: nil, middle_name: nil,
10
+ last_name: nil, suffix: nil, take: nil, skip: nil)
11
+ @client.get("#{base}/Contacts", {
12
+ licenseName: license_name, tradeName: trade_name, fflNumber: ffl_number,
13
+ organizationName: organization_name, firstName: first_name,
14
+ middleName: middle_name, lastName: last_name, suffix: suffix,
15
+ take: take, skip: skip
16
+ })
17
+ end
18
+
19
+ def find(id)
20
+ @client.get("#{base}/Contacts/#{id}")
21
+ end
22
+
23
+ def find_by_external_id(external_id)
24
+ @client.get("#{base}/Contacts/GetByExternalId/#{external_id}")
25
+ end
26
+
27
+ def create(params = {})
28
+ @client.post("#{base}/Contacts", params)
29
+ end
30
+
31
+ def update(id, params = {})
32
+ @client.put("#{base}/Contacts/#{id}", params)
33
+ end
34
+
35
+ def destroy(id)
36
+ @client.delete("#{base}/Contacts/#{id}")
37
+ end
38
+
39
+ def merge(winning_contact_id:, losing_contact_id:)
40
+ @client.post("#{base}/Contacts/Merge", {
41
+ winningContactId: winning_contact_id,
42
+ losingContactId: losing_contact_id
43
+ })
44
+ end
45
+
46
+ # License operations
47
+
48
+ def get_license(id, license_id)
49
+ @client.get("#{base}/Contacts/#{id}/Licenses/#{license_id}")
50
+ end
51
+
52
+ def create_license(id, params = {})
53
+ @client.post("#{base}/Contacts/#{id}/Licenses", params)
54
+ end
55
+
56
+ def update_license(id, license_id, params = {})
57
+ @client.put("#{base}/Contacts/#{id}/Licenses/#{license_id}", params)
58
+ end
59
+
60
+ def delete_license(id, license_id)
61
+ @client.delete("#{base}/Contacts/#{id}/Licenses/#{license_id}")
62
+ end
63
+
64
+ private
65
+
66
+ def base
67
+ @client.base_path
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,129 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Dispositions
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(take: nil, skip: nil, include_4473: nil, id: nil, external_id: nil,
9
+ type: nil, ttsn: nil, otsn: nil, purchase_order_number: nil,
10
+ invoice_number: nil, shipment_tracking_number: nil,
11
+ is_manufacturing_disposition: nil, disposed_to_contact_id: nil,
12
+ disposed_to_contact_external_id: nil, item_id: nil, item_external_id: nil)
13
+ @client.get("#{base}/Dispositions", {
14
+ take: take, skip: skip, include4473: include_4473, id: id,
15
+ externalId: external_id, type: type, TTSN: ttsn, OTSN: otsn,
16
+ purchaseOrderNumber: purchase_order_number, invoiceNumber: invoice_number,
17
+ shipmentTrackingNumber: shipment_tracking_number,
18
+ isManufacturingDisposition: is_manufacturing_disposition,
19
+ disposedToContactId: disposed_to_contact_id,
20
+ disposedToContactExternalId: disposed_to_contact_external_id,
21
+ itemId: item_id, itemExternalId: item_external_id
22
+ })
23
+ end
24
+
25
+ def list_4473s(take: nil, skip: nil, include_awaiting_completion: nil)
26
+ @client.get("#{base}/Dispositions/Only4473s", {
27
+ take: take, skip: skip,
28
+ includeAwaiting4473Completion: include_awaiting_completion
29
+ })
30
+ end
31
+
32
+ def find(id)
33
+ @client.get("#{base}/Dispositions/#{id}")
34
+ end
35
+
36
+ def find_by_external_id(external_id)
37
+ @client.get("#{base}/Dispositions/GetByExternalId/#{external_id}")
38
+ end
39
+
40
+ def create(params = {})
41
+ @client.post("#{base}/Dispositions", params)
42
+ end
43
+
44
+ def create_nfa(params = {})
45
+ @client.post("#{base}/Dispositions/NFA", params)
46
+ end
47
+
48
+ def create_theft_loss(params = {})
49
+ @client.post("#{base}/Dispositions/TheftLoss", params)
50
+ end
51
+
52
+ def create_destroyed(params = {})
53
+ @client.post("#{base}/Dispositions/Destroyed", params)
54
+ end
55
+
56
+ def update(id, params = {})
57
+ @client.put("#{base}/Dispositions/#{id}", params)
58
+ end
59
+
60
+ def destroy(id)
61
+ @client.delete("#{base}/Dispositions/#{id}")
62
+ end
63
+
64
+ def attach_contact(id, contact_id)
65
+ @client.put("#{base}/Dispositions/#{id}/AttachContact/#{contact_id}", {})
66
+ end
67
+
68
+ def lock(id)
69
+ @client.put("#{base}/Dispositions/Lock/#{id}", {})
70
+ end
71
+
72
+ def lock_by_external_id(external_id)
73
+ @client.put("#{base}/Dispositions/LockByExternalId/#{external_id}", {})
74
+ end
75
+
76
+ def commit(id, params = {}, list_disposed_items: nil)
77
+ path = "#{base}/Dispositions/#{id}/Commit"
78
+ path += "?listDisposedItems=true" if list_disposed_items
79
+ @client.post(path, params)
80
+ end
81
+
82
+ def create_and_commit(params = {}, list_disposed_items: nil)
83
+ path = "#{base}/Dispositions/CreateAndCommit"
84
+ path += "?listDisposedItems=true" if list_disposed_items
85
+ @client.post(path, params)
86
+ end
87
+
88
+ def create_as_pending(params = {})
89
+ @client.post("#{base}/Dispositions/CreateAsPending", params)
90
+ end
91
+
92
+ # Item operations
93
+
94
+ def list_items(id)
95
+ @client.get("#{base}/Dispositions/#{id}/Items")
96
+ end
97
+
98
+ def add_items(id, items = [])
99
+ @client.post("#{base}/Dispositions/#{id}/Items", { items: items })
100
+ end
101
+
102
+ def add_items_by_external_id(disposition_external_id, items = [])
103
+ @client.post("#{base}/Dispositions/#{disposition_external_id}/Items/AddByExternalId", { items: items })
104
+ end
105
+
106
+ def add_items_by_search(disposition_external_id, params = {})
107
+ @client.post("#{base}/Dispositions/#{disposition_external_id}/Items/AddBySearch", params)
108
+ end
109
+
110
+ def edit_item_price(id, item_id, price:)
111
+ @client.put("#{base}/Dispositions/#{id}/Items/EditPrice/#{item_id}", { price: price })
112
+ end
113
+
114
+ def remove_item(id, item_id)
115
+ @client.delete("#{base}/Dispositions/#{id}/Items/Remove/#{item_id}")
116
+ end
117
+
118
+ def remove_item_by_external_id(id, item_external_id)
119
+ @client.delete("#{base}/Dispositions/#{id}/Items/RemoveByExternalId/#{item_external_id}")
120
+ end
121
+
122
+ private
123
+
124
+ def base
125
+ @client.base_path
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,13 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Downloads
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def bound_book(params = {})
9
+ @client.post_binary("#{@client.base_path}/Downloads/BoundBook", params)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Form4473s
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def download(form4473_id)
9
+ @client.get_binary("#{@client.base_path}/Form4473s/Download/#{form4473_id}")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Inventory
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def bulk_verify(serials:, rollback_partial: nil, update_location: nil,
9
+ location: nil, verified_utc: nil)
10
+ @client.put("#{@client.base_path}/Inventory/BulkVerify", {
11
+ serials: serials,
12
+ rollbackPartial: rollback_partial,
13
+ updateLocation: update_location,
14
+ location: location,
15
+ verifiedUtc: verified_utc
16
+ }.reject { |_, v| v.nil? })
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,84 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Items
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(search: nil, item_number: nil, serial: nil, manufacturer: nil,
9
+ importer: nil, model: nil, type: nil, caliber: nil, location: nil,
10
+ condition: nil, mpn: nil, upc: nil, sku: nil, is_theft_loss: nil,
11
+ is_destroyed: nil, do_not_dispose: nil, disposition_id: nil,
12
+ status: nil, acquired_on_or_after: nil, acquired_on_or_before: nil,
13
+ acquire_purchase_order_number: nil, acquire_invoice_number: nil,
14
+ acquire_shipment_tracking_number: nil, disposed_on_or_after: nil,
15
+ disposed_on_or_before: nil, dispose_purchase_order_number: nil,
16
+ dispose_invoice_number: nil, dispose_shipment_tracking_number: nil,
17
+ has_external_id: nil, acquisition_type: nil, ttsn: nil, otsn: nil,
18
+ take: nil, skip: nil)
19
+ @client.get("#{base}/Items", {
20
+ search: search, itemNumber: item_number, serial: serial,
21
+ manufacturer: manufacturer, importer: importer, model: model,
22
+ type: type, caliber: caliber, location: location, condition: condition,
23
+ mpn: mpn, upc: upc, sku: sku, isTheftLoss: is_theft_loss,
24
+ isDestroyed: is_destroyed, doNotDispose: do_not_dispose,
25
+ dispositionId: disposition_id, status: status,
26
+ acquiredOnOrAfter: acquired_on_or_after,
27
+ acquiredOnOrBefore: acquired_on_or_before,
28
+ acquirePurchaseOrderNumber: acquire_purchase_order_number,
29
+ acquireInvoiceNumber: acquire_invoice_number,
30
+ acquireShipmentTrackingNumber: acquire_shipment_tracking_number,
31
+ disposedOnOrAfter: disposed_on_or_after,
32
+ disposedOnOrBefore: disposed_on_or_before,
33
+ disposePurchaseOrderNumber: dispose_purchase_order_number,
34
+ disposeInvoiceNumber: dispose_invoice_number,
35
+ disposeShipmentTrackingNumber: dispose_shipment_tracking_number,
36
+ hasExternalId: has_external_id, acquisitionType: acquisition_type,
37
+ ttsn: ttsn, otsn: otsn, take: take, skip: skip
38
+ })
39
+ end
40
+
41
+ def find(id)
42
+ @client.get("#{base}/Items/#{id}")
43
+ end
44
+
45
+ def find_by_external_id(external_id)
46
+ @client.get("#{base}/Items/GetByExternalId/#{external_id}")
47
+ end
48
+
49
+ def update(id, params = {})
50
+ @client.put("#{base}/Items/#{id}", params)
51
+ end
52
+
53
+ def delete(id, delete_type:, delete_note: nil)
54
+ @client.post("#{base}/Items/#{id}/Delete", {
55
+ deleteType: delete_type,
56
+ deleteNote: delete_note
57
+ }.reject { |_, v| v.nil? })
58
+ end
59
+
60
+ def set_acquisition_contact(id, contact_id)
61
+ @client.put("#{base}/Items/#{id}/AcquisitionContact/#{contact_id}", {})
62
+ end
63
+
64
+ def undispose(id, note: nil)
65
+ body = note ? { note: note } : {}
66
+ @client.put("#{base}/Items/#{id}/Undispose", body)
67
+ end
68
+
69
+ def set_external_id(id, external_id:)
70
+ @client.put("#{base}/Items/#{id}/SetExternalId", { externalId: external_id })
71
+ end
72
+
73
+ def set_external_ids(items = [])
74
+ @client.put("#{base}/Items/SetExternalIds", { items: items })
75
+ end
76
+
77
+ private
78
+
79
+ def base
80
+ @client.base_path
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,15 @@
1
+ module Fastbound
2
+ module Resources
3
+ class MultipleSaleReports
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def download(multiple_sale_report_id, attachment_id)
9
+ @client.get_binary(
10
+ "#{@client.base_path}/MultipleSaleReports/Download/#{multiple_sale_report_id}/a/#{attachment_id}"
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,71 @@
1
+ module Fastbound
2
+ module Resources
3
+ class SmartLists
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def acquire_types
9
+ @client.get("#{base}/SmartLists/AcquireType")
10
+ end
11
+
12
+ def calibers
13
+ @client.get("#{base}/SmartLists/Caliber")
14
+ end
15
+
16
+ def conditions
17
+ @client.get("#{base}/SmartLists/Condition")
18
+ end
19
+
20
+ def countries_of_manufacture
21
+ @client.get("#{base}/SmartLists/CountryOfManufacture")
22
+ end
23
+
24
+ def delete_types
25
+ @client.get("#{base}/SmartLists/DeleteType")
26
+ end
27
+
28
+ def dispose_types
29
+ @client.get("#{base}/SmartLists/DisposeType")
30
+ end
31
+
32
+ def importers
33
+ @client.get("#{base}/SmartLists/Importer")
34
+ end
35
+
36
+ def item_types
37
+ @client.get("#{base}/SmartLists/ItemType")
38
+ end
39
+
40
+ def license_types
41
+ @client.get("#{base}/SmartLists/LicenseType")
42
+ end
43
+
44
+ def locations
45
+ @client.get("#{base}/SmartLists/Location")
46
+ end
47
+
48
+ def manufacturers
49
+ @client.get("#{base}/SmartLists/Manufacturer")
50
+ end
51
+
52
+ def theft_loss_types
53
+ @client.get("#{base}/SmartLists/TheftLossType")
54
+ end
55
+
56
+ def manufacturing_dispose_types
57
+ @client.get("#{base}/SmartLists/ManufacturingDisposeType")
58
+ end
59
+
60
+ def manufacturing_acquire_types
61
+ @client.get("#{base}/SmartLists/ManufacturingAcquireType")
62
+ end
63
+
64
+ private
65
+
66
+ def base
67
+ @client.base_path
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,13 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Users
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(include_disabled: nil)
9
+ @client.get("#{@client.base_path}/Users", { IncludeDisabled: include_disabled })
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Fastbound
2
+ module Resources
3
+ class Webhooks
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list_events
9
+ @client.get("#{base}/Webhooks/Events")
10
+ end
11
+
12
+ def find(name)
13
+ @client.get("#{base}/Webhooks/#{name}")
14
+ end
15
+
16
+ def create(name:, url:, description: nil, events: [])
17
+ @client.post("#{base}/Webhooks", {
18
+ name: name, url: url, description: description, events: events
19
+ }.reject { |_, v| v.nil? })
20
+ end
21
+
22
+ def update(name, url: nil, description: nil, events: nil)
23
+ @client.put("#{base}/Webhooks/#{name}", {
24
+ name: name, url: url, description: description, events: events
25
+ }.reject { |_, v| v.nil? })
26
+ end
27
+
28
+ def destroy(name)
29
+ @client.delete("#{base}/Webhooks/#{name}")
30
+ end
31
+
32
+ private
33
+
34
+ def base
35
+ @client.base_path
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Fastbound
2
+ VERSION = "0.1.0"
3
+ end
data/lib/fastbound.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "fastbound/version"
2
+ require "fastbound/error"
3
+ require "fastbound/client"
4
+ require "fastbound/resources/account"
5
+ require "fastbound/resources/acquisitions"
6
+ require "fastbound/resources/attachments"
7
+ require "fastbound/resources/contacts"
8
+ require "fastbound/resources/dispositions"
9
+ require "fastbound/resources/downloads"
10
+ require "fastbound/resources/form4473s"
11
+ require "fastbound/resources/inventory"
12
+ require "fastbound/resources/items"
13
+ require "fastbound/resources/multiple_sale_reports"
14
+ require "fastbound/resources/smart_lists"
15
+ require "fastbound/resources/users"
16
+ require "fastbound/resources/webhooks"
17
+
18
+ module Fastbound
19
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastbound
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - JD Warren
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday-multipart
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ description:
48
+ email:
49
+ - johndavid400@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - README.md
55
+ - lib/fastbound.rb
56
+ - lib/fastbound/client.rb
57
+ - lib/fastbound/error.rb
58
+ - lib/fastbound/resources/account.rb
59
+ - lib/fastbound/resources/acquisitions.rb
60
+ - lib/fastbound/resources/attachments.rb
61
+ - lib/fastbound/resources/contacts.rb
62
+ - lib/fastbound/resources/dispositions.rb
63
+ - lib/fastbound/resources/downloads.rb
64
+ - lib/fastbound/resources/form4473s.rb
65
+ - lib/fastbound/resources/inventory.rb
66
+ - lib/fastbound/resources/items.rb
67
+ - lib/fastbound/resources/multiple_sale_reports.rb
68
+ - lib/fastbound/resources/smart_lists.rb
69
+ - lib/fastbound/resources/users.rb
70
+ - lib/fastbound/resources/webhooks.rb
71
+ - lib/fastbound/version.rb
72
+ homepage: https://github.com/johndavid400/fastbound
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 2.7.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.0.9
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Ruby API wrapper for the FastBound firearms compliance API
95
+ test_files: []