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.
- checksums.yaml +7 -0
- data/.github/workflows/release.yml +30 -0
- data/.gitignore +13 -0
- data/AGENTS.md +47 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +173 -0
- data/Rakefile +21 -0
- data/examples/list_lot_stock.rb +22 -0
- data/lib/shedcloud/partner_api/auth.rb +26 -0
- data/lib/shedcloud/partner_api/auth_provider.rb +95 -0
- data/lib/shedcloud/partner_api/client.rb +72 -0
- data/lib/shedcloud/partner_api/errors.rb +54 -0
- data/lib/shedcloud/partner_api/hosts.rb +31 -0
- data/lib/shedcloud/partner_api/http_client.rb +117 -0
- data/lib/shedcloud/partner_api/http_transport.rb +57 -0
- data/lib/shedcloud/partner_api/request_options.rb +32 -0
- data/lib/shedcloud/partner_api/resources/agreements.rb +29 -0
- data/lib/shedcloud/partner_api/resources/base.rb +23 -0
- data/lib/shedcloud/partner_api/resources/configurator_sessions.rb +13 -0
- data/lib/shedcloud/partner_api/resources/customers.rb +29 -0
- data/lib/shedcloud/partner_api/resources/documents.rb +17 -0
- data/lib/shedcloud/partner_api/resources/domains.rb +17 -0
- data/lib/shedcloud/partner_api/resources/events.rb +53 -0
- data/lib/shedcloud/partner_api/resources/leads.rb +33 -0
- data/lib/shedcloud/partner_api/resources/locations.rb +25 -0
- data/lib/shedcloud/partner_api/resources/lot_stock.rb +15 -0
- data/lib/shedcloud/partner_api/resources/orders.rb +61 -0
- data/lib/shedcloud/partner_api/resources/payments.rb +17 -0
- data/lib/shedcloud/partner_api/resources/products.rb +29 -0
- data/lib/shedcloud/partner_api/resources/quotes.rb +49 -0
- data/lib/shedcloud/partner_api/resources/stock_templates.rb +19 -0
- data/lib/shedcloud/partner_api/resources/users.rb +29 -0
- data/lib/shedcloud/partner_api/resources/work_orders.rb +33 -0
- data/lib/shedcloud/partner_api/scopes.rb +61 -0
- data/lib/shedcloud/partner_api/version.rb +7 -0
- data/lib/shedcloud/partner_api/webhooks.rb +72 -0
- data/lib/shedcloud/partner_api.rb +35 -0
- 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
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
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
|