shedcloud-partner_api 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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/release.yml +30 -0
  3. data/.gitignore +13 -0
  4. data/AGENTS.md +47 -0
  5. data/CHANGELOG.md +11 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE +21 -0
  8. data/README.md +173 -0
  9. data/Rakefile +21 -0
  10. data/examples/list_lot_stock.rb +22 -0
  11. data/lib/shedcloud/partner_api/auth.rb +26 -0
  12. data/lib/shedcloud/partner_api/auth_provider.rb +95 -0
  13. data/lib/shedcloud/partner_api/client.rb +72 -0
  14. data/lib/shedcloud/partner_api/errors.rb +54 -0
  15. data/lib/shedcloud/partner_api/hosts.rb +31 -0
  16. data/lib/shedcloud/partner_api/http_client.rb +117 -0
  17. data/lib/shedcloud/partner_api/http_transport.rb +57 -0
  18. data/lib/shedcloud/partner_api/request_options.rb +32 -0
  19. data/lib/shedcloud/partner_api/resources/agreements.rb +29 -0
  20. data/lib/shedcloud/partner_api/resources/base.rb +23 -0
  21. data/lib/shedcloud/partner_api/resources/configurator_sessions.rb +13 -0
  22. data/lib/shedcloud/partner_api/resources/customers.rb +29 -0
  23. data/lib/shedcloud/partner_api/resources/documents.rb +17 -0
  24. data/lib/shedcloud/partner_api/resources/domains.rb +17 -0
  25. data/lib/shedcloud/partner_api/resources/events.rb +53 -0
  26. data/lib/shedcloud/partner_api/resources/leads.rb +33 -0
  27. data/lib/shedcloud/partner_api/resources/locations.rb +25 -0
  28. data/lib/shedcloud/partner_api/resources/lot_stock.rb +15 -0
  29. data/lib/shedcloud/partner_api/resources/orders.rb +61 -0
  30. data/lib/shedcloud/partner_api/resources/payments.rb +17 -0
  31. data/lib/shedcloud/partner_api/resources/products.rb +29 -0
  32. data/lib/shedcloud/partner_api/resources/quotes.rb +49 -0
  33. data/lib/shedcloud/partner_api/resources/stock_templates.rb +19 -0
  34. data/lib/shedcloud/partner_api/resources/users.rb +29 -0
  35. data/lib/shedcloud/partner_api/resources/work_orders.rb +33 -0
  36. data/lib/shedcloud/partner_api/scopes.rb +61 -0
  37. data/lib/shedcloud/partner_api/version.rb +7 -0
  38. data/lib/shedcloud/partner_api/webhooks.rb +72 -0
  39. data/lib/shedcloud/partner_api.rb +35 -0
  40. metadata +112 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 145e122f76cf50761a1e721feb510f3bb0ea708809dfb986213da9c59c4c1505
4
+ data.tar.gz: 98b24f0679f7722abec77cab65c9303e21a2ea63934ffdecf6ee50f45ae2b35c
5
+ SHA512:
6
+ metadata.gz: 9a247a283d4fb7a477ae1c93f68e089799a6667f849168689dddd96e8edf04fd2b7cbaf08322959580f66fded4266aecfc4d56e515de69c1a7066259642ed8c5
7
+ data.tar.gz: 8dac558602c6efe9774323b06305a6a4c87eac64d521c0d97cf2a27531206c92dd9531198c9bf5975ba9e541711bb65eccd22e2f0163f8068b31ab816652a426
@@ -0,0 +1,30 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ contents: write
14
+ id-token: write
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ persist-credentials: false
20
+
21
+ - uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: '3.2'
24
+ bundler-cache: true
25
+
26
+ - name: Run tests
27
+ run: bundle exec rspec
28
+
29
+ - name: Publish to RubyGems
30
+ uses: rubygems/release-gem@v1
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /Gemfile.lock
3
+ /pkg/
4
+ /tmp/
5
+ /.rspec_status
6
+ /coverage/
7
+
8
+ # IDE / OS
9
+ .idea/
10
+ .vscode/
11
+ *.swp
12
+ .DS_Store
13
+ Thumbs.db
data/AGENTS.md ADDED
@@ -0,0 +1,47 @@
1
+ # AGENTS.md — shedcloud-gem
2
+
3
+ Orientation for coding agents working in this repository.
4
+
5
+ ## What this is
6
+
7
+ `shedcloud-partner_api` is the official Ruby gem for ShedCloud's public **Partner API** (`/partner/v1/*`). Partners authenticate with a company-scoped API key (`sc_live_…`) or OAuth2 client credentials (`POST /oauth/token`).
8
+
9
+ This repo is the **consumer library**. The API itself lives in `shedcloud-api-go` (`internal/handler/partnerapi`, `docs/PARTNER_API.md`, `docs/partner-api/swagger.yaml`). The TypeScript twin is `shedcloud-npm` (`@shedcloud/partner-api`); the Go twin is `shedcloud-gomod` (`partnerapi`); the PHP twin is `shedcloud-php` (`shedcloud/partner-api`).
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ shedcloud-gem/
15
+ ├── lib/shedcloud/
16
+ │ ├── partner_api.rb # require entry
17
+ │ └── partner_api/
18
+ │ ├── client.rb # Client
19
+ │ ├── hosts.rb # production / sandbox base URLs
20
+ │ ├── auth.rb # API key + OAuth credentials
21
+ │ ├── auth_provider.rb # token cache
22
+ │ ├── http_client.rb # request helper + query encoding
23
+ │ ├── errors.rb
24
+ │ ├── scopes.rb
25
+ │ ├── webhooks.rb
26
+ │ └── resources/ # one file per /partner/v1 resource
27
+ ├── spec/
28
+ ├── examples/
29
+ └── shedcloud-partner_api.gemspec
30
+ ```
31
+
32
+ ## Rules of the road
33
+
34
+ 1. **Never invent endpoints or field names.** Mirror `shedcloud-api-go/docs/PARTNER_API.md` and keep parity with the other SDKs.
35
+ 2. **Hosts** are fixed in `hosts.rb`: production `https://go.shedcloud.com`, sandbox `https://api.shedcloudtest.com`.
36
+ 3. **Scopes** live in `scopes.rb` and must stay in sync with `shedcloud-api-go/internal/partnerauth/scopes.go`.
37
+ 4. **Auth stays outside resource classes.** Resources only call `HttpClient#request`. Token exchange/caching belongs in `auth_provider.rb`.
38
+ 5. **Keep the surface small.** No portal admin endpoints (`/v1/settings/api-keys`, etc.).
39
+ 6. After changing `lib/`, run `bundle exec rspec`.
40
+
41
+ ## Adding a resource
42
+
43
+ 1. Confirm the route + scopes in `shedcloud-api-go/internal/router/partner_api_routes.go`.
44
+ 2. Add `lib/shedcloud/partner_api/resources/<name>.rb` with `list` / `get` / `update` / `update_status` as applicable.
45
+ 3. Wire it on `Client` in `client.rb` and require it from `partner_api.rb`.
46
+ 4. Add an RSpec example with a mocked transport.
47
+ 5. Mirror the same change in the other SDK repos when practical.
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial `shedcloud-partner_api` Ruby gem
6
+ - Built-in hosts: production `https://go.shedcloud.com` (default), sandbox `https://api.shedcloudtest.com`
7
+ - Auth: API key bearer + OAuth2 client-credentials with token cache
8
+ - Resources: lot-stock, stock-templates, leads, quotes, orders, work-orders, locations, customers, domains, agreements, products, users, payments, documents, events, configurator-sessions
9
+ - Typed errors (`PartnerApiError`, `AuthError`) and scope constants
10
+ - Webhook signature verification
11
+ - Parity with `@shedcloud/partner-api`, `shedcloud-gomod/partnerapi`, and `shedcloud/partner-api`
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Corland Partners LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # shedcloud-gem
2
+
3
+ Official Ruby gem for the ShedCloud Partner API (`/partner/v1/*`).
4
+
5
+ Use this gem from Ruby 3.2+ to call company-scoped Partner API endpoints with an API key (`sc_live_…`) or OAuth2 client credentials.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ gem install shedcloud-partner_api
11
+ ```
12
+
13
+ Or add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'shedcloud-partner_api'
17
+ ```
18
+
19
+ ## Hosts
20
+
21
+ | Environment | Host |
22
+ |-------------|------|
23
+ | `production` (default) | `https://go.shedcloud.com` |
24
+ | `sandbox` | `https://api.shedcloudtest.com` |
25
+
26
+ Pass `environment:` for sandbox, or `base_url:` for a custom/local override.
27
+
28
+ ## Quick start
29
+
30
+ ### API key (production)
31
+
32
+ ```ruby
33
+ require 'shedcloud/partner_api'
34
+
35
+ client = ShedCloud::PartnerApi::Client.new(
36
+ auth: ShedCloud::PartnerApi::Auth.new(api_key: ENV.fetch('SHEDCLOUD_API_KEY'))
37
+ )
38
+
39
+ stock = client.lot_stock.list(
40
+ limit: 50,
41
+ purchaseType: 'Lot Stock',
42
+ sort: 'price',
43
+ order: 'asc'
44
+ )
45
+
46
+ puts stock['total']
47
+ puts stock['data'].first['title']
48
+ ```
49
+
50
+ ### Sandbox
51
+
52
+ ```ruby
53
+ client = ShedCloud::PartnerApi::Client.new(
54
+ environment: 'sandbox',
55
+ auth: ShedCloud::PartnerApi::Auth.new(api_key: ENV.fetch('SHEDCLOUD_API_KEY'))
56
+ )
57
+ ```
58
+
59
+ ### OAuth2 client credentials
60
+
61
+ ```ruby
62
+ client = ShedCloud::PartnerApi::Client.new(
63
+ auth: ShedCloud::PartnerApi::Auth.new(
64
+ client_id: ENV.fetch('SHEDCLOUD_CLIENT_ID'),
65
+ client_secret: ENV.fetch('SHEDCLOUD_CLIENT_SECRET')
66
+ )
67
+ )
68
+
69
+ orders = client.orders.list(status: 'Unprocessed', limit: 25)
70
+ ```
71
+
72
+ Create credentials in the ShedCloud portal under **Settings → Developer API**.
73
+
74
+ ## Resources
75
+
76
+ Each resource maps to a section of the [hosted reference](https://go.shedcloud.com/partner/reference).
77
+
78
+ | Client property | Endpoints |
79
+ |-----------------|-----------|
80
+ | `client.lot_stock` | `GET /partner/v1/lot-stock` |
81
+ | `client.stock_templates` | `GET /partner/v1/stock-templates` |
82
+ | `client.leads` | `GET/POST/PATCH /partner/v1/leads`, status + status-history |
83
+ | `client.quotes` | `GET/POST/PATCH /partner/v1/quotes`, convert, line-items |
84
+ | `client.orders` | `GET/POST/PATCH /partner/v1/orders`, contract, payments, payment-links |
85
+ | `client.work_orders` | `GET/POST/PATCH /partner/v1/work-orders`, status + status-history |
86
+ | `client.locations` | `GET/POST/PATCH /partner/v1/locations` |
87
+ | `client.customers` | `GET/POST/PATCH /partner/v1/customers`, merge |
88
+ | `client.products` | `GET/POST/PATCH /partner/v1/products`, create sizes |
89
+ | `client.domains` | `GET /partner/v1/domains`, location domains |
90
+ | `client.agreements` | `GET /partner/v1/agreements`, state-legal |
91
+ | `client.users` | `GET/POST/PATCH /partner/v1/users`, roles |
92
+ | `client.payments` | `GET /partner/v1/payments` (read-only) |
93
+ | `client.documents` | `GET /partner/v1/documents`, download |
94
+ | `client.events` | `GET /partner/v1/events`, each iterator, redeliver, deliveries |
95
+ | `client.configurator_sessions` | `POST /partner/v1/configurator-sessions` |
96
+
97
+ ### Idempotency and optimistic concurrency
98
+
99
+ ```ruby
100
+ quote = client.quotes.create(
101
+ {
102
+ serialNumber: 'SC-2024-00123',
103
+ customer: { name: 'Jane Doe', email: 'jane@example.com' }
104
+ },
105
+ options: ShedCloud::PartnerApi::RequestOptions.with_idempotency_key(SecureRandom.uuid)
106
+ )
107
+
108
+ client.orders.update(
109
+ order['id'],
110
+ { customerPhone: '555-0100' },
111
+ options: ShedCloud::PartnerApi::RequestOptions.with_if_match(order['version'])
112
+ )
113
+ ```
114
+
115
+ For boolean query params that must be sent even when `false`:
116
+
117
+ ```ruby
118
+ client.quotes.list(converted: ShedCloud::PartnerApi::QueryValue.new(false))
119
+ ```
120
+
121
+ ## Webhooks
122
+
123
+ Verify webhook deliveries with the subscription secret against the **raw** request body:
124
+
125
+ ```ruby
126
+ body = request.body.read
127
+ ShedCloud::PartnerApi::Webhooks.verify_signature(
128
+ ENV.fetch('SHEDCLOUD_WEBHOOK_SECRET'),
129
+ request.headers['X-ShedCloud-Signature'],
130
+ body
131
+ )
132
+ ```
133
+
134
+ ## Errors
135
+
136
+ Failed responses raise `ShedCloud::PartnerApi::PartnerApiError` (or `AuthError` for OAuth token failures):
137
+
138
+ ```ruby
139
+ begin
140
+ client.orders.get(id)
141
+ rescue ShedCloud::PartnerApi::PartnerApiError => e
142
+ puts e.status, e.message
143
+ # e.forbidden?, e.not_found?, e.rate_limited?
144
+ end
145
+ ```
146
+
147
+ ## Scopes
148
+
149
+ ```ruby
150
+ ShedCloud::PartnerApi::Scopes::LOT_STOCK_READ # partner-api.lot-stock.read
151
+ ShedCloud::PartnerApi::Scopes::ORDERS_WRITE # partner-api.orders.write
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ bundle install
158
+ bundle exec rspec
159
+ ```
160
+
161
+ ## Versioning & changelog
162
+
163
+ The Partner API is **additive-only within `/partner/v1`**. This gem is tagged with semver in lockstep with API additions.
164
+
165
+ - API changes: [hosted changelog](https://go.shedcloud.com/partner/reference#changelog)
166
+
167
+ ## Docs
168
+
169
+ - Partner API reference: https://go.shedcloud.com/partner/reference
170
+ - Backend source of truth: `shedcloud-api-go/docs/PARTNER_API.md`
171
+ - TypeScript twin: [`@shedcloud/partner-api`](https://github.com/Corland-Partners-LLC/shedcloud-npm)
172
+ - Go twin: [`shedcloud-gomod/partnerapi`](https://github.com/Corland-Partners-LLC/shedcloud-gomod)
173
+ - PHP twin: [`shedcloud/partner-api`](https://github.com/Corland-Partners-LLC/shedcloud-php)
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ # Tag pushes run in a detached HEAD. Skip bundler's git tag/push steps and only
11
+ # build + push the gem (release-gem configures RubyGems credentials via OIDC).
12
+ if ENV['GITHUB_ACTIONS']
13
+ Rake::Task['release'].clear
14
+
15
+ task release: :build do
16
+ gem = Dir.glob('pkg/*.gem').max
17
+ raise 'No gem found in pkg/' unless gem
18
+
19
+ sh "gem push #{gem}"
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'shedcloud/partner_api'
5
+
6
+ client = ShedCloud::PartnerApi::Client.new(
7
+ auth: ShedCloud::PartnerApi::Auth.new(api_key: ENV.fetch('SHEDCLOUD_API_KEY', ''))
8
+ )
9
+
10
+ stock = client.lot_stock.list(
11
+ limit: 5,
12
+ purchaseType: 'Lot Stock',
13
+ sort: 'price',
14
+ order: 'asc'
15
+ )
16
+
17
+ puts "total=#{stock['total']} returned=#{stock['data']&.length || 0}"
18
+
19
+ if stock['data']&.any?
20
+ item = stock['data'].first
21
+ puts format('first: %s | %s | $%.2f', item['serialNumber'], item['title'], item['price'].to_f)
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShedCloud
4
+ module PartnerApi
5
+ class Auth
6
+ attr_reader :api_key, :client_id, :client_secret
7
+
8
+ def initialize(api_key: nil, client_id: nil, client_secret: nil)
9
+ @api_key = api_key
10
+ @client_id = client_id
11
+ @client_secret = client_secret
12
+ end
13
+
14
+ def validate!
15
+ has_key = !api_key.to_s.strip.empty?
16
+ has_oauth = !client_id.to_s.strip.empty? && !client_secret.to_s.strip.empty?
17
+
18
+ raise ArgumentError, 'auth: set exactly one of api_key or client_id+client_secret' if has_key == has_oauth
19
+ end
20
+
21
+ def api_key?
22
+ !api_key.to_s.strip.empty?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ module ShedCloud
7
+ module PartnerApi
8
+ class AuthProvider
9
+ def initialize(base_url:, auth:, transport: HttpTransport.new, token_skew_seconds: 60)
10
+ @base_url = Hosts.trim_trailing_slashes(base_url)
11
+ @auth = auth
12
+ @transport = transport
13
+ @token_skew_seconds = token_skew_seconds
14
+ @cached_access_token = nil
15
+ @cached_expires_at = nil
16
+ end
17
+
18
+ def access_token
19
+ return @auth.api_key.to_s.strip if @auth.api_key?
20
+
21
+ now = Time.now.to_i
22
+ if @cached_access_token && @cached_expires_at && now < (@cached_expires_at - @token_skew_seconds)
23
+ return @cached_access_token
24
+ end
25
+
26
+ refresh_oauth_token
27
+ end
28
+
29
+ def refresh
30
+ return @auth.api_key.to_s.strip if @auth.api_key?
31
+
32
+ @cached_access_token = nil
33
+ @cached_expires_at = nil
34
+ refresh_oauth_token
35
+ end
36
+
37
+ private
38
+
39
+ def refresh_oauth_token
40
+ url = "#{@base_url}/oauth/token"
41
+ credentials = ["#{@auth.client_id}:#{@auth.client_secret}"].pack('m0')
42
+
43
+ response = @transport.send(
44
+ 'POST',
45
+ url,
46
+ {
47
+ 'Accept' => 'application/json',
48
+ 'Authorization' => "Basic #{credentials}",
49
+ 'Content-Type' => 'application/x-www-form-urlencoded'
50
+ },
51
+ URI.encode_www_form(grant_type: 'client_credentials')
52
+ )
53
+
54
+ parsed = parse_json(response[:body])
55
+
56
+ if response[:status] < 200 || response[:status] >= 300
57
+ message, code = extract_error(parsed, response[:status].to_s)
58
+ raise AuthError.new(message, status: response[:status], body: parsed, code: code)
59
+ end
60
+
61
+ token = parsed.is_a?(Hash) ? parsed['access_token'] : nil
62
+ if token.to_s.empty?
63
+ raise AuthError.new('OAuth token response missing access_token', status: response[:status], body: parsed)
64
+ end
65
+
66
+ expires_in = parsed['expires_in'].to_i
67
+ expires_in = 3600 if expires_in <= 0
68
+
69
+ @cached_access_token = token
70
+ @cached_expires_at = Time.now.to_i + expires_in
71
+ @cached_access_token
72
+ end
73
+
74
+ def parse_json(raw)
75
+ JSON.parse(raw)
76
+ rescue JSON::ParserError
77
+ raw
78
+ end
79
+
80
+ def extract_error(body, fallback)
81
+ message = fallback
82
+ code = nil
83
+
84
+ return [message, code] unless body.is_a?(Hash)
85
+
86
+ message = body['error_description'] if body['error_description'].is_a?(String) && !body['error_description'].empty?
87
+ message = body['error'] if message == fallback && body['error'].is_a?(String) && !body['error'].empty?
88
+ message = body['message'] if message == fallback && body['message'].is_a?(String) && !body['message'].empty?
89
+ code = body['error'] if body['error'].is_a?(String)
90
+
91
+ [message, code]
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShedCloud
4
+ module PartnerApi
5
+ class Client
6
+ attr_reader :base_url,
7
+ :lot_stock,
8
+ :stock_templates,
9
+ :leads,
10
+ :quotes,
11
+ :orders,
12
+ :work_orders,
13
+ :locations,
14
+ :customers,
15
+ :domains,
16
+ :agreements,
17
+ :products,
18
+ :users,
19
+ :payments,
20
+ :documents,
21
+ :events,
22
+ :configurator_sessions
23
+
24
+ def initialize(auth:, environment: nil, base_url: nil, transport: HttpTransport.new, timeout_seconds: 30.0,
25
+ user_agent: 'shedcloud-ruby/partner-api', token_skew_seconds: 60)
26
+ raise ArgumentError, 'Client: auth is required' unless auth.is_a?(Auth)
27
+
28
+ auth.validate!
29
+
30
+ @base_url = Hosts.resolve_base_url(base_url: base_url, environment: environment)
31
+ @auth_provider = AuthProvider.new(
32
+ base_url: @base_url,
33
+ auth: auth,
34
+ transport: transport,
35
+ token_skew_seconds: token_skew_seconds
36
+ )
37
+
38
+ @http = HttpClient.new(
39
+ base_url: @base_url,
40
+ get_access_token: -> { @auth_provider.access_token },
41
+ transport: transport,
42
+ user_agent: user_agent
43
+ )
44
+
45
+ @lot_stock = Resources::LotStock.new(@http)
46
+ @stock_templates = Resources::StockTemplates.new(@http)
47
+ @leads = Resources::Leads.new(@http)
48
+ @quotes = Resources::Quotes.new(@http)
49
+ @orders = Resources::Orders.new(@http)
50
+ @work_orders = Resources::WorkOrders.new(@http)
51
+ @locations = Resources::Locations.new(@http)
52
+ @customers = Resources::Customers.new(@http)
53
+ @domains = Resources::Domains.new(@http)
54
+ @agreements = Resources::Agreements.new(@http)
55
+ @products = Resources::Products.new(@http)
56
+ @users = Resources::Users.new(@http)
57
+ @payments = Resources::Payments.new(@http)
58
+ @documents = Resources::Documents.new(@http)
59
+ @events = Resources::Events.new(@http)
60
+ @configurator_sessions = Resources::ConfiguratorSessions.new(@http)
61
+ end
62
+
63
+ def me
64
+ @http.request('GET', '/partner/v1/me') || {}
65
+ end
66
+
67
+ def refresh_access_token
68
+ @auth_provider.refresh
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShedCloud
4
+ module PartnerApi
5
+ class PartnerApiError < StandardError
6
+ attr_reader :status, :body, :code
7
+
8
+ def initialize(message, status:, body: nil, code: nil)
9
+ @status = status
10
+ @body = body
11
+ @code = code
12
+ super(format_message(message))
13
+ end
14
+
15
+ def unauthorized? = status == 401
16
+ def forbidden? = status == 403
17
+ def not_found? = status == 404
18
+ def rate_limited? = status == 429
19
+
20
+ private
21
+
22
+ def format_message(message)
23
+ if code && !code.empty?
24
+ "#{message} (status=#{status} code=#{code})"
25
+ else
26
+ "#{message} (status=#{status})"
27
+ end
28
+ end
29
+ end
30
+
31
+ class AuthError < StandardError
32
+ attr_reader :status, :body, :code
33
+
34
+ def initialize(message, status:, body: nil, code: nil)
35
+ @status = status
36
+ @body = body
37
+ @code = code
38
+ super(format_message(message))
39
+ end
40
+
41
+ def unauthorized? = status == 401
42
+
43
+ private
44
+
45
+ def format_message(message)
46
+ if code && !code.empty?
47
+ "partner-api auth: #{message} (status=#{status} code=#{code})"
48
+ else
49
+ "partner-api auth: #{message} (status=#{status})"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShedCloud
4
+ module PartnerApi
5
+ module Hosts
6
+ HOST_PRODUCTION = 'https://go.shedcloud.com'
7
+ HOST_SANDBOX = 'https://api.shedcloudtest.com'
8
+ DEFAULT_BASE_URL = HOST_PRODUCTION
9
+
10
+ ENVIRONMENT_PRODUCTION = 'production'
11
+ ENVIRONMENT_SANDBOX = 'sandbox'
12
+
13
+ module_function
14
+
15
+ def resolve_base_url(base_url: nil, environment: nil)
16
+ trimmed = trim_trailing_slashes(base_url.to_s)
17
+ return trimmed unless trimmed.empty?
18
+
19
+ case environment
20
+ when ENVIRONMENT_SANDBOX then HOST_SANDBOX
21
+ when ENVIRONMENT_PRODUCTION, nil, '' then HOST_PRODUCTION
22
+ else HOST_PRODUCTION
23
+ end
24
+ end
25
+
26
+ def trim_trailing_slashes(value)
27
+ value.to_s.sub(%r{/+\z}, '')
28
+ end
29
+ end
30
+ end
31
+ end