halo_msp_api 0.1.0 → 0.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/CHANGELOG.md +25 -0
- data/Gemfile +3 -8
- data/README.md +32 -32
- data/Rakefile +3 -3
- data/examples/basic_usage.rb +95 -31
- data/lib/halo_msp_api/client.rb +49 -28
- data/lib/halo_msp_api/configuration.rb +1 -0
- data/lib/halo_msp_api/error.rb +1 -0
- data/lib/halo_msp_api/resources/actions.rb +21 -20
- data/lib/halo_msp_api/resources/agents.rb +24 -23
- data/lib/halo_msp_api/resources/appointments.rb +18 -17
- data/lib/halo_msp_api/resources/assets.rb +33 -32
- data/lib/halo_msp_api/resources/base.rb +12 -10
- data/lib/halo_msp_api/resources/clients.rb +26 -30
- data/lib/halo_msp_api/resources/integrations.rb +109 -106
- data/lib/halo_msp_api/resources/invoices.rb +32 -31
- data/lib/halo_msp_api/resources/knowledge_base.rb +17 -16
- data/lib/halo_msp_api/resources/organisations.rb +11 -10
- data/lib/halo_msp_api/resources/products.rb +33 -0
- data/lib/halo_msp_api/resources/purchase_orders.rb +16 -15
- data/lib/halo_msp_api/resources/quotations.rb +14 -13
- data/lib/halo_msp_api/resources/reports.rb +23 -22
- data/lib/halo_msp_api/resources/sales_orders.rb +16 -15
- data/lib/halo_msp_api/resources/services.rb +39 -38
- data/lib/halo_msp_api/resources/slas.rb +21 -20
- data/lib/halo_msp_api/resources/suppliers.rb +16 -15
- data/lib/halo_msp_api/resources/tickets.rb +63 -45
- data/lib/halo_msp_api/resources/users.rb +20 -24
- data/lib/halo_msp_api/resources/webhooks.rb +22 -21
- data/lib/halo_msp_api/version.rb +1 -1
- data/lib/halo_msp_api.rb +26 -24
- metadata +12 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66a71218fe73c35441a0f239ab1c80a3879aac9d9c44116c167a260c6cf6cb3c
|
|
4
|
+
data.tar.gz: 8602e863b06f0e4fd6ea315da702c6e9f1f8e41fdbdb3f19c22e08c4dd5ab91c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4fbc442112b4f78af465735d8b59b74f3cfed6308306d42810a550729aafe6ed8a61815d904841a648e956a11c1aed92f15bfe512423a9c848895962d9d4be8
|
|
7
|
+
data.tar.gz: 300aebb149d31b354667abc6845598739e74f8b60008702f8c418b3ab0159646e53c40996e1e11c12591097bb5df9b5c33acf227ece2fa56a517978f95605ad7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2025-09-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Added methods for grabbing ticket statuses, categories, and other user defined attributes
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Fixed resource methods not using the correct method from the base class
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2025-09-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Products Resource Class
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Renamed Create/Update/Get/Delete methods in resource classes that caused an infinite recursion error
|
|
23
|
+
- Fixed specs that were failing due to naming mismatched/mocking API requests
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Updated `basic_usage.rb` examples to print specific data for verification
|
|
27
|
+
- Updated project to adhere to rubocop linting rules
|
|
28
|
+
- Removed support for Ruby 2.6 and 2.7 due to dependency incompatibilities
|
|
29
|
+
|
|
30
|
+
### Deleted
|
|
31
|
+
- Removed `/me/` calls from Clients and Users since those calls returned 500 from the HaloPSA API
|
|
32
|
+
|
|
8
33
|
## [0.1.0] - 2025-07-10
|
|
9
34
|
|
|
10
35
|
### Added
|
data/Gemfile
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
source
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
5
|
# Specify your gem's dependencies in halo_api.gemspec
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
|
-
gem
|
|
8
|
+
gem 'rake', '~> 13.0'
|
|
9
9
|
|
|
10
10
|
group :development, :test do
|
|
11
|
-
gem
|
|
12
|
-
gem "rubocop", "~> 1.21"
|
|
13
|
-
gem "yard", "~> 0.9"
|
|
14
|
-
gem "webmock", "~> 3.0"
|
|
15
|
-
gem "vcr", "~> 6.0"
|
|
16
|
-
gem "pry", "~> 0.14"
|
|
11
|
+
gem 'pry'
|
|
17
12
|
end
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# HaloMspApi Ruby Gem
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/halo_msp_api)
|
|
4
|
+
|
|
3
5
|
A comprehensive Ruby API wrapper for the Halo ITSM, HaloPSA and HaloCRM REST API.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -53,13 +55,13 @@ client = HaloApi::Client.new(config)
|
|
|
53
55
|
|
|
54
56
|
```ruby
|
|
55
57
|
# List all tickets
|
|
56
|
-
tickets = client.tickets.
|
|
58
|
+
tickets = client.tickets.tickets
|
|
57
59
|
|
|
58
60
|
# Get a specific ticket
|
|
59
|
-
ticket = client.tickets.
|
|
61
|
+
ticket = client.tickets.ticket(123)
|
|
60
62
|
|
|
61
63
|
# Create a new ticket
|
|
62
|
-
new_ticket = client.tickets.
|
|
64
|
+
new_ticket = client.tickets.create_ticket({
|
|
63
65
|
summary: "New ticket",
|
|
64
66
|
details: "Ticket description",
|
|
65
67
|
tickettype_id: 1,
|
|
@@ -67,23 +69,20 @@ new_ticket = client.tickets.create({
|
|
|
67
69
|
})
|
|
68
70
|
|
|
69
71
|
# Update a ticket
|
|
70
|
-
client.tickets.
|
|
72
|
+
client.tickets.update_ticket(123, { summary: "Updated summary" })
|
|
71
73
|
|
|
72
74
|
# Delete a ticket
|
|
73
|
-
client.tickets.
|
|
75
|
+
client.tickets.delete_ticket(123)
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
### Working with Users
|
|
77
79
|
|
|
78
80
|
```ruby
|
|
79
81
|
# List all users
|
|
80
|
-
users = client.users.
|
|
81
|
-
|
|
82
|
-
# Get current user
|
|
83
|
-
current_user = client.users.me
|
|
82
|
+
users = client.users.users
|
|
84
83
|
|
|
85
84
|
# Create a new user
|
|
86
|
-
new_user = client.users.
|
|
85
|
+
new_user = client.users.create_user({
|
|
87
86
|
name: "John Doe",
|
|
88
87
|
emailaddress: "john@example.com"
|
|
89
88
|
})
|
|
@@ -93,13 +92,13 @@ new_user = client.users.create({
|
|
|
93
92
|
|
|
94
93
|
```ruby
|
|
95
94
|
# List all assets
|
|
96
|
-
assets = client.assets.
|
|
95
|
+
assets = client.assets.assets
|
|
97
96
|
|
|
98
97
|
# Get a specific asset
|
|
99
|
-
asset = client.assets.
|
|
98
|
+
asset = client.assets.asset(123)
|
|
100
99
|
|
|
101
100
|
# Create a new asset
|
|
102
|
-
new_asset = client.assets.
|
|
101
|
+
new_asset = client.assets.create_asset({
|
|
103
102
|
inventory_number: "ASSET001",
|
|
104
103
|
assettype_id: 1
|
|
105
104
|
})
|
|
@@ -109,13 +108,13 @@ new_asset = client.assets.create({
|
|
|
109
108
|
|
|
110
109
|
```ruby
|
|
111
110
|
# List all clients
|
|
112
|
-
clients = client.clients.
|
|
111
|
+
clients = client.clients.clients
|
|
113
112
|
|
|
114
113
|
# Get a specific client
|
|
115
|
-
client_record = client.clients.
|
|
114
|
+
client_record = client.clients.client(123)
|
|
116
115
|
|
|
117
116
|
# Create a new client
|
|
118
|
-
new_client = client.clients.
|
|
117
|
+
new_client = client.clients.create_client({
|
|
119
118
|
name: "ACME Corp",
|
|
120
119
|
website: "https://acme.com"
|
|
121
120
|
})
|
|
@@ -125,13 +124,13 @@ new_client = client.clients.create({
|
|
|
125
124
|
|
|
126
125
|
```ruby
|
|
127
126
|
# List all invoices
|
|
128
|
-
invoices = client.invoices.
|
|
127
|
+
invoices = client.invoices.invoices
|
|
129
128
|
|
|
130
129
|
# Get a specific invoice
|
|
131
|
-
invoice = client.invoices.
|
|
130
|
+
invoice = client.invoices.invoice(123)
|
|
132
131
|
|
|
133
132
|
# Create a new invoice
|
|
134
|
-
new_invoice = client.invoices.
|
|
133
|
+
new_invoice = client.invoices.create_invoice({
|
|
135
134
|
client_id: 1,
|
|
136
135
|
invoicedate: "2023-01-01"
|
|
137
136
|
})
|
|
@@ -144,10 +143,10 @@ pdf_data = client.invoices.pdf(123)
|
|
|
144
143
|
|
|
145
144
|
```ruby
|
|
146
145
|
# List all reports
|
|
147
|
-
reports = client.reports.
|
|
146
|
+
reports = client.reports.reports
|
|
148
147
|
|
|
149
148
|
# Get a specific report
|
|
150
|
-
report = client.reports.
|
|
149
|
+
report = client.reports.report(123)
|
|
151
150
|
|
|
152
151
|
# Get report data
|
|
153
152
|
report_data = client.reports.data("published_report_id")
|
|
@@ -157,10 +156,10 @@ report_data = client.reports.data("published_report_id")
|
|
|
157
156
|
|
|
158
157
|
```ruby
|
|
159
158
|
# Get Azure AD data
|
|
160
|
-
azure_data = client.integrations.
|
|
159
|
+
azure_data = client.integrations.azure_ad
|
|
161
160
|
|
|
162
161
|
# Get Slack data
|
|
163
|
-
slack_data = client.integrations.
|
|
162
|
+
slack_data = client.integrations.slack
|
|
164
163
|
|
|
165
164
|
# Import Jira data
|
|
166
165
|
client.integrations.import_jira(jira_data)
|
|
@@ -185,6 +184,7 @@ The gem provides access to the following Halo API resources:
|
|
|
185
184
|
- **Quotations** - `client.quotations` - Quote management, approvals
|
|
186
185
|
- **Sales Orders** - `client.sales_orders` - Sales order management
|
|
187
186
|
- **Suppliers** - `client.suppliers` - Supplier and contract management
|
|
187
|
+
- **Products** - `client.products` - Product management
|
|
188
188
|
|
|
189
189
|
### Service Management
|
|
190
190
|
- **Appointments** - `client.appointments` - Scheduling and availability
|
|
@@ -207,20 +207,20 @@ The gem provides specific error classes for different types of API errors:
|
|
|
207
207
|
|
|
208
208
|
```ruby
|
|
209
209
|
begin
|
|
210
|
-
ticket = client.tickets.
|
|
211
|
-
rescue
|
|
210
|
+
ticket = client.tickets.ticket(999999)
|
|
211
|
+
rescue HaloMspApi::NotFoundError
|
|
212
212
|
puts "Ticket not found"
|
|
213
|
-
rescue
|
|
213
|
+
rescue HaloMspApi::AuthenticationError
|
|
214
214
|
puts "Authentication failed"
|
|
215
|
-
rescue
|
|
215
|
+
rescue HaloMspApi::AuthorizationError
|
|
216
216
|
puts "Access forbidden"
|
|
217
|
-
rescue
|
|
217
|
+
rescue HaloMspApi::ValidationError => e
|
|
218
218
|
puts "Validation error: #{e.message}"
|
|
219
|
-
rescue
|
|
219
|
+
rescue HaloMspApi::RateLimitError
|
|
220
220
|
puts "Rate limit exceeded"
|
|
221
|
-
rescue
|
|
221
|
+
rescue HaloMspApi::ServerError
|
|
222
222
|
puts "Server error"
|
|
223
|
-
rescue
|
|
223
|
+
rescue HaloMspApi::APIError => e
|
|
224
224
|
puts "API error: #{e.message} (Status: #{e.status_code})"
|
|
225
225
|
end
|
|
226
226
|
```
|
|
@@ -235,6 +235,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
235
235
|
|
|
236
236
|
## Development
|
|
237
237
|
|
|
238
|
-
After checking out the repo, run `
|
|
238
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
239
239
|
|
|
240
240
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
data/examples/basic_usage.rb
CHANGED
|
@@ -1,70 +1,134 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'halo_msp_api'
|
|
6
6
|
|
|
7
7
|
# Configure the HaloMspApi gem
|
|
8
8
|
HaloMspApi.configure do |config|
|
|
9
|
-
config.base_url = ENV[
|
|
10
|
-
config.client_id = ENV[
|
|
11
|
-
config.client_secret = ENV[
|
|
12
|
-
config.tenant = ENV[
|
|
9
|
+
config.base_url = ENV['HALO_BASE_URL'] || 'https://your-instance.haloitsm.com/api'
|
|
10
|
+
config.client_id = ENV['HALO_CLIENT_ID'] || 'your_client_id'
|
|
11
|
+
config.client_secret = ENV['HALO_CLIENT_SECRET'] || 'your_client_secret'
|
|
12
|
+
config.tenant = ENV['HALO_TENANT'] # Optional
|
|
13
13
|
config.timeout = 30
|
|
14
14
|
config.retries = 3
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Get a client instance
|
|
18
|
-
|
|
18
|
+
instance = HaloMspApi.client
|
|
19
19
|
|
|
20
20
|
begin
|
|
21
|
-
puts
|
|
21
|
+
puts '=== HaloApi Ruby Gem Example Usage ==='
|
|
22
22
|
puts
|
|
23
23
|
|
|
24
24
|
# Example 1: List tickets
|
|
25
|
-
puts
|
|
26
|
-
tickets =
|
|
27
|
-
puts "Found #{tickets
|
|
25
|
+
puts '1. Fetching tickets...'
|
|
26
|
+
tickets = instance.tickets.tickets(count: 5)
|
|
27
|
+
puts "Found #{tickets['tickets']&.length || 0} tickets"
|
|
28
|
+
if tickets['tickets']&.any?
|
|
29
|
+
puts 'Ticket summaries:'
|
|
30
|
+
tickets['tickets'].each do |ticket|
|
|
31
|
+
puts "- #{ticket['summary'] || 'Unknown'}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
28
34
|
puts
|
|
29
35
|
|
|
30
|
-
# Example 2:
|
|
31
|
-
puts
|
|
32
|
-
|
|
33
|
-
puts "
|
|
36
|
+
# Example 2: List users
|
|
37
|
+
puts '2. Fetching users...'
|
|
38
|
+
users = instance.users.users(count: 5)
|
|
39
|
+
puts "Found #{users['users']&.length || 0} users"
|
|
40
|
+
if users['users']&.any?
|
|
41
|
+
puts 'User names:'
|
|
42
|
+
users['users'].each do |user|
|
|
43
|
+
puts "- #{user['name'] || 'Unknown'}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
34
46
|
puts
|
|
35
47
|
|
|
36
|
-
# Example 3: List
|
|
37
|
-
puts
|
|
38
|
-
clients =
|
|
39
|
-
puts "Found #{clients
|
|
48
|
+
# Example 3: List companies
|
|
49
|
+
puts '3. Fetching clients...'
|
|
50
|
+
clients = instance.clients.clients(count: 5)
|
|
51
|
+
puts "Found #{clients['clients']&.length || 0} clients"
|
|
52
|
+
if clients['clients']&.any?
|
|
53
|
+
puts 'Client names:'
|
|
54
|
+
clients['clients'].each do |client_item|
|
|
55
|
+
puts "- #{client_item['name'] || 'Unknown'}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
40
58
|
puts
|
|
41
59
|
|
|
42
60
|
# Example 4: List assets
|
|
43
|
-
puts
|
|
44
|
-
assets =
|
|
45
|
-
puts "Found #{assets
|
|
61
|
+
puts '4. Fetching assets...'
|
|
62
|
+
assets = instance.assets.assets(count: 5)
|
|
63
|
+
puts "Found #{assets['assets']&.length || 0} assets"
|
|
64
|
+
if assets['assets']&.any?
|
|
65
|
+
puts 'Asset Inventory Number & Client Association:'
|
|
66
|
+
assets['assets'].each do |asset|
|
|
67
|
+
puts "- #{asset['inventory_number'] || 'Unknown'}: #{asset['client_name'] || 'Unknown'}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
46
70
|
puts
|
|
47
71
|
|
|
48
72
|
# Example 5: List invoices
|
|
49
|
-
puts
|
|
50
|
-
invoices =
|
|
51
|
-
puts "Found #{invoices
|
|
73
|
+
puts '5. Fetching invoices...'
|
|
74
|
+
invoices = instance.invoices.invoices(count: 5)
|
|
75
|
+
puts "Found #{invoices['invoices']&.length || 0} invoices"
|
|
76
|
+
if invoices['invoices']&.any?
|
|
77
|
+
puts 'Invoice ID and Client Association:'
|
|
78
|
+
invoices['invoices'].each do |invoice|
|
|
79
|
+
puts "- #{invoice['id'] || 'Unknown'}: #{invoice['client_name'] || 'Unknown'}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
52
82
|
puts
|
|
53
83
|
|
|
54
84
|
# Example 6: Get reports
|
|
55
|
-
puts
|
|
56
|
-
reports =
|
|
57
|
-
puts "Found #{reports
|
|
85
|
+
puts '6. Fetching reports...'
|
|
86
|
+
reports = instance.reports.reports(count: 5)
|
|
87
|
+
puts "Found #{reports['reports']&.length || 0} reports"
|
|
88
|
+
if reports['reports']&.any?
|
|
89
|
+
puts 'Report Names:'
|
|
90
|
+
reports['reports'].each do |report|
|
|
91
|
+
puts "- #{report['name'] || 'Unknown'}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
58
94
|
puts
|
|
59
95
|
|
|
60
|
-
|
|
96
|
+
# Example 7: Get contracts
|
|
97
|
+
puts '7. Fetching contracts...'
|
|
98
|
+
contracts = instance.clients.contracts(count: 5)
|
|
99
|
+
puts "Found #{contracts['contracts']&.length || 0} contracts"
|
|
100
|
+
if contracts['contracts']&.any?
|
|
101
|
+
puts 'Contract Client & Active:'
|
|
102
|
+
contracts['contracts'].each do |contract|
|
|
103
|
+
puts "- #{contract['client_name'] || 'Unknown'}: #{contract['active'] || 'Unknown'}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
puts
|
|
107
|
+
|
|
108
|
+
# Example 8: Get products
|
|
109
|
+
puts '8. Fetching products...'
|
|
110
|
+
products = instance.products.products(count: 5)
|
|
111
|
+
puts "Found #{products['items']&.length || 0} products"
|
|
112
|
+
if products['items']&.any?
|
|
113
|
+
puts 'Product Names:'
|
|
114
|
+
products['items'].each do |product|
|
|
115
|
+
puts "- #{product['name'] || 'Unknown'}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
puts
|
|
119
|
+
|
|
120
|
+
puts '9. Listing Statuses'
|
|
121
|
+
statuses = instance.tickets.statuses
|
|
122
|
+
puts "Found #{statuses&.length || 0} statuses"
|
|
123
|
+
puts "First Status: #{statuses.first['name']}"
|
|
61
124
|
|
|
125
|
+
puts '=== Example completed successfully! ==='
|
|
62
126
|
rescue HaloMspApi::AuthenticationError => e
|
|
63
127
|
puts "Authentication failed: #{e.message}"
|
|
64
|
-
puts
|
|
128
|
+
puts 'Please check your client_id and client_secret'
|
|
65
129
|
rescue HaloMspApi::AuthorizationError => e
|
|
66
130
|
puts "Authorization failed: #{e.message}"
|
|
67
|
-
puts
|
|
131
|
+
puts 'Please check your permissions'
|
|
68
132
|
rescue HaloMspApi::APIError => e
|
|
69
133
|
puts "API Error: #{e.message}"
|
|
70
134
|
puts "Status Code: #{e.status_code}" if e.respond_to?(:status_code)
|
data/lib/halo_msp_api/client.rb
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/retry'
|
|
5
|
+
require 'json'
|
|
6
6
|
|
|
7
7
|
module HaloMspApi
|
|
8
|
+
# Client class for Halo MSP API
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
10
|
class Client
|
|
9
11
|
attr_reader :configuration, :connection
|
|
10
12
|
|
|
11
13
|
def initialize(configuration = nil)
|
|
12
|
-
@configuration = configuration ||
|
|
13
|
-
raise ConfigurationError,
|
|
14
|
+
@configuration = configuration || HaloMspApi.configuration
|
|
15
|
+
raise ConfigurationError, 'Configuration is required' unless @configuration&.valid?
|
|
14
16
|
|
|
15
17
|
@connection = build_connection
|
|
16
18
|
@access_token = nil
|
|
@@ -38,6 +40,10 @@ module HaloMspApi
|
|
|
38
40
|
@clients ||= Resources::Clients.new(self)
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
def contracts
|
|
44
|
+
@contracts ||= Resources::Contracts.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
def integrations
|
|
42
48
|
@integrations ||= Resources::Integrations.new(self)
|
|
43
49
|
end
|
|
@@ -54,6 +60,10 @@ module HaloMspApi
|
|
|
54
60
|
@organisations ||= Resources::Organisations.new(self)
|
|
55
61
|
end
|
|
56
62
|
|
|
63
|
+
def products
|
|
64
|
+
@products ||= Resources::Products.new(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
57
67
|
def purchase_orders
|
|
58
68
|
@purchase_orders ||= Resources::PurchaseOrders.new(self)
|
|
59
69
|
end
|
|
@@ -117,13 +127,15 @@ module HaloMspApi
|
|
|
117
127
|
|
|
118
128
|
private
|
|
119
129
|
|
|
130
|
+
# rubocop:disable Metrics/AbcSize
|
|
131
|
+
# rubocop:disable Metrics/MethodLength
|
|
120
132
|
def request(method, path, data = {})
|
|
121
133
|
ensure_authenticated!
|
|
122
134
|
|
|
123
135
|
response = connection.send(method) do |req|
|
|
124
136
|
req.url path
|
|
125
|
-
req.headers[
|
|
126
|
-
req.headers[
|
|
137
|
+
req.headers['Authorization'] = "Bearer #{@access_token}"
|
|
138
|
+
req.headers['Content-Type'] = 'application/json'
|
|
127
139
|
|
|
128
140
|
if %i[post put patch].include?(method) && !data.empty?
|
|
129
141
|
req.body = data.to_json
|
|
@@ -134,31 +146,37 @@ module HaloMspApi
|
|
|
134
146
|
|
|
135
147
|
handle_response(response)
|
|
136
148
|
rescue Faraday::TimeoutError
|
|
137
|
-
raise TimeoutError,
|
|
149
|
+
raise TimeoutError, 'Request timed out'
|
|
138
150
|
rescue Faraday::ConnectionFailed
|
|
139
|
-
raise ConnectionError,
|
|
151
|
+
raise ConnectionError, 'Connection failed'
|
|
140
152
|
end
|
|
153
|
+
# rubocop:enable Metrics/AbcSize
|
|
154
|
+
# rubocop:enable Metrics/MethodLength
|
|
141
155
|
|
|
156
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
157
|
+
# rubocop:disable Metrics/MethodLength
|
|
142
158
|
def handle_response(response)
|
|
143
159
|
case response.status
|
|
144
160
|
when 200..299
|
|
145
161
|
parse_response(response)
|
|
146
162
|
when 401
|
|
147
|
-
raise AuthenticationError,
|
|
163
|
+
raise AuthenticationError, 'Authentication failed'
|
|
148
164
|
when 403
|
|
149
|
-
raise AuthorizationError,
|
|
165
|
+
raise AuthorizationError, 'Access forbidden'
|
|
150
166
|
when 404
|
|
151
|
-
raise NotFoundError,
|
|
167
|
+
raise NotFoundError, 'Resource not found'
|
|
152
168
|
when 422
|
|
153
169
|
raise ValidationError, "Validation error: #{response.body}"
|
|
154
170
|
when 429
|
|
155
|
-
raise RateLimitError,
|
|
171
|
+
raise RateLimitError, 'Rate limit exceeded'
|
|
156
172
|
when 500..599
|
|
157
173
|
raise ServerError, "Server error: #{response.status}"
|
|
158
174
|
else
|
|
159
|
-
raise APIError.new(
|
|
175
|
+
raise APIError.new('Unexpected response', status_code: response.status, response_body: response.body)
|
|
160
176
|
end
|
|
161
177
|
end
|
|
178
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
179
|
+
# rubocop:enable Metrics/MethodLength
|
|
162
180
|
|
|
163
181
|
def parse_response(response)
|
|
164
182
|
return nil if response.body.nil? || response.body.empty?
|
|
@@ -178,32 +196,34 @@ module HaloMspApi
|
|
|
178
196
|
@access_token && @token_expires_at && Time.now < @token_expires_at
|
|
179
197
|
end
|
|
180
198
|
|
|
199
|
+
# rubocop:disable Metrics/AbcSize
|
|
200
|
+
# rubocop:disable Metrics/MethodLength
|
|
181
201
|
def authenticate!
|
|
182
202
|
auth_params = {
|
|
183
|
-
grant_type:
|
|
203
|
+
grant_type: 'client_credentials',
|
|
184
204
|
client_id: configuration.client_id,
|
|
185
205
|
client_secret: configuration.client_secret,
|
|
186
|
-
scope:
|
|
206
|
+
scope: 'all'
|
|
187
207
|
}
|
|
188
|
-
|
|
208
|
+
|
|
189
209
|
# Include tenant if configured (required for multi-tenant instances)
|
|
190
210
|
auth_params[:tenant] = configuration.tenant if configuration.tenant
|
|
191
|
-
|
|
192
|
-
auth_response = connection.post(
|
|
193
|
-
req.headers[
|
|
211
|
+
|
|
212
|
+
auth_response = connection.post('/auth/token') do |req|
|
|
213
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
194
214
|
req.body = URI.encode_www_form(auth_params)
|
|
195
215
|
end
|
|
196
216
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
raise AuthenticationError, "Failed to authenticate: #{auth_response.body}"
|
|
203
|
-
end
|
|
217
|
+
raise AuthenticationError, "Failed to authenticate: #{auth_response.body}" unless auth_response.status == 200
|
|
218
|
+
|
|
219
|
+
token_data = JSON.parse(auth_response.body)
|
|
220
|
+
@access_token = token_data['access_token']
|
|
221
|
+
@token_expires_at = Time.now + token_data['expires_in'].to_i
|
|
204
222
|
rescue JSON::ParserError
|
|
205
|
-
raise AuthenticationError,
|
|
223
|
+
raise AuthenticationError, 'Invalid authentication response'
|
|
206
224
|
end
|
|
225
|
+
# rubocop:enable Metrics/AbcSize
|
|
226
|
+
# rubocop:enable Metrics/MethodLength
|
|
207
227
|
|
|
208
228
|
def build_connection
|
|
209
229
|
Faraday.new(url: configuration.base_url) do |conn|
|
|
@@ -214,3 +234,4 @@ module HaloMspApi
|
|
|
214
234
|
end
|
|
215
235
|
end
|
|
216
236
|
end
|
|
237
|
+
# rubocop:enable Metrics/ClassLength
|