printavo-ruby 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +90 -4
- data/docs/CHANGELOG.md +25 -0
- data/docs/FUTURE.md +1 -16
- data/lib/printavo/graphql_client.rb +84 -4
- data/lib/printavo/resources/customers.rb +85 -0
- data/lib/printavo/resources/inquiries.rb +69 -0
- data/lib/printavo/resources/orders.rb +127 -0
- data/lib/printavo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c8061b68869c47ca86f6c37dc51d60020c035364ea70c68e68bad1df2f4da6c
|
|
4
|
+
data.tar.gz: 1d9c8f4d5805ab82bca17f28891fdc21899f2b166638c4e100e8531c8275073f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b39cbc93da67a0b81114fa7209720d944d6ee87cd77763876191ce907a38498cf9c3020548b4f3f23f2a3a675bdd5b7f6ccf80ba937e1c929372e6cfe00761d
|
|
7
|
+
data.tar.gz: ab0e28809466df5269eece7960b2d425c1d4f15db68361c55aa730909bd3662b8ac9f1e256c49218e7cc03febe71c96661d64a2c472bb96bc2aa8c0f8b97ab13
|
data/README.md
CHANGED
|
@@ -136,6 +136,57 @@ end
|
|
|
136
136
|
all_jobs = client.jobs.all_pages(order_id: "99")
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
### Mutations
|
|
140
|
+
|
|
141
|
+
#### Customers
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# Create
|
|
145
|
+
customer = client.customers.create(
|
|
146
|
+
primary_contact: { firstName: "Jane", lastName: "Smith", email: "jane@example.com" },
|
|
147
|
+
company_name: "Acme Shirts"
|
|
148
|
+
)
|
|
149
|
+
puts customer.full_name # => "Jane Smith"
|
|
150
|
+
puts customer.company # => "Acme Shirts"
|
|
151
|
+
|
|
152
|
+
# Update
|
|
153
|
+
customer = client.customers.update("42", company_name: "New Name Inc")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Orders
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# Create (Printavo creates orders as quotes first)
|
|
160
|
+
order = client.orders.create(
|
|
161
|
+
contact: { id: "456" },
|
|
162
|
+
due_at: "2026-06-01T09:00:00Z",
|
|
163
|
+
customer_due_at: "2026-06-01",
|
|
164
|
+
nickname: "Summer Rush"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Update
|
|
168
|
+
order = client.orders.update("99", nickname: "Rush Job", production_note: "Ships Friday")
|
|
169
|
+
|
|
170
|
+
# Move to a new status
|
|
171
|
+
registry = client.statuses.registry
|
|
172
|
+
order = client.orders.update_status("99", status_id: registry[:in_production].id)
|
|
173
|
+
puts order.status # => "In Production"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Inquiries
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Create
|
|
180
|
+
inquiry = client.inquiries.create(
|
|
181
|
+
name: "Bob Johnson",
|
|
182
|
+
email: "bob@example.com",
|
|
183
|
+
request: "100 hoodies, front + back print"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Update
|
|
187
|
+
inquiry = client.inquiries.update("55", nickname: "Hoodies Rush")
|
|
188
|
+
```
|
|
189
|
+
|
|
139
190
|
### Statuses
|
|
140
191
|
|
|
141
192
|
```ruby
|
|
@@ -196,6 +247,42 @@ result = client.graphql.query(
|
|
|
196
247
|
)
|
|
197
248
|
```
|
|
198
249
|
|
|
250
|
+
### Mutations
|
|
251
|
+
|
|
252
|
+
Use `mutate` for GraphQL write operations:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
result = client.graphql.mutate(
|
|
256
|
+
<<~GQL,
|
|
257
|
+
mutation UpdateOrder($id: ID!, $nickname: String!) {
|
|
258
|
+
updateOrder(id: $id, input: { nickname: $nickname }) {
|
|
259
|
+
order { id nickname }
|
|
260
|
+
errors
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
GQL
|
|
264
|
+
variables: { id: "99", nickname: "Rush Job" }
|
|
265
|
+
)
|
|
266
|
+
result["updateOrder"]["order"]["nickname"] # => "Rush Job"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Raw GraphQL Pagination
|
|
270
|
+
|
|
271
|
+
Paginate any custom query without a resource wrapper using `paginate`.
|
|
272
|
+
The `path:` option is dot-separated and maps to the connection in the response:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
client.graphql.paginate(MY_QUERY, path: "orders", first: 50) do |nodes|
|
|
276
|
+
nodes.each { |n| puts n["nickname"] }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Nested connection (e.g. order.lineItems)
|
|
280
|
+
client.graphql.paginate(JOBS_QUERY, path: "order.lineItems",
|
|
281
|
+
variables: { orderId: "99" }) do |nodes|
|
|
282
|
+
nodes.each { |j| puts j["name"] }
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
199
286
|
## Webhooks
|
|
200
287
|
|
|
201
288
|
`Printavo::Webhooks.verify` provides Rack-compatible HMAC-SHA256 signature verification.
|
|
@@ -257,12 +344,11 @@ end
|
|
|
257
344
|
| 0.1.0 | Auth + Customers + Orders + Jobs + Webhooks (Rack-compatible) ✅ |
|
|
258
345
|
| 0.2.0 | Status registry + Inquiries ✅ |
|
|
259
346
|
| 0.3.0 | Pagination helpers (`each_page`, `all_pages`) + `bin/lint` ✅ |
|
|
260
|
-
| 0.4.0 | Expanded GraphQL DSL |
|
|
261
|
-
| 0.5.0 | Mutations (create/update) |
|
|
347
|
+
| 0.4.0 | Expanded GraphQL DSL (`mutate`, `paginate`) ✅ |
|
|
348
|
+
| 0.5.0 | Mutations (create/update) ✅ |
|
|
262
349
|
| 0.6.0 | Analytics / Reporting queries |
|
|
263
350
|
| 0.7.0 | Community burn-in / API stabilization |
|
|
264
|
-
| 0.8.0 |
|
|
265
|
-
| 0.9.0 | Retry/backoff + rate limit awareness |
|
|
351
|
+
| 0.8.0 | Retry/backoff + rate limit awareness |
|
|
266
352
|
| 1.0.0 | Stable public SDK |
|
|
267
353
|
|
|
268
354
|
**Rules**: `PATCH` = bug fix · `MINOR` = new backward-compatible feature · `MAJOR` = breaking change
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.5.0] - 2026-03-30
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `Customers#create(primary_contact:, **input)` — `customerCreate` mutation; normalizes `primaryContact` + `companyName` response fields to match the read-side `Customer` model
|
|
15
|
+
- `Customers#update(id, **input)` — `customerUpdate` mutation
|
|
16
|
+
- `Orders#create(**input)` — `quoteCreate` mutation; normalizes `total` → `totalPrice` in response
|
|
17
|
+
- `Orders#update(id, **input)` — `quoteUpdate` mutation
|
|
18
|
+
- `Orders#update_status(id, status_id:)` — `statusUpdate` mutation; handles `OrderUnion (Quote | Invoice)` via inline fragments
|
|
19
|
+
- `Inquiries#create(**input)` — `inquiryCreate` mutation
|
|
20
|
+
- `Inquiries#update(id, **input)` — `inquiryUpdate` mutation
|
|
21
|
+
- All mutation methods accept snake_case keyword args and camelize them for GraphQL input
|
|
22
|
+
- `CREATE_MUTATION` and `UPDATE_MUTATION` constants on `Customers`, `Orders`, `Inquiries`; `UPDATE_STATUS_MUTATION` on `Orders`
|
|
23
|
+
- Private `build_customer` and `build_order` normalizers centralize mutation response mapping
|
|
24
|
+
- Private `camelize_keys` on `Customers` and `Orders` converts `snake_case` → `camelCase` for input variables
|
|
25
|
+
- Factory helpers `fake_customer_mutation_response` and `fake_order_mutation_response` for testing
|
|
26
|
+
|
|
27
|
+
## [0.4.0] - 2026-03-30
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `GraphqlClient#mutate` — semantic method for GraphQL mutations; identical transport to `query` but signals write intent at the call site; lays the foundation for 0.5.0 resource-level mutations
|
|
31
|
+
- `GraphqlClient#paginate` — cursor-following pagination for raw GraphQL queries; accepts a dot-separated `path:` to resolve nested connections (e.g. `"order.lineItems"`); yields each page's `nodes` array
|
|
32
|
+
- `GraphqlClient#execute` (private) — extracted shared POST logic; `query` and `mutate` both delegate to it
|
|
33
|
+
- `GraphqlClient#dig_path` (private) — resolves dot-separated key paths against nested response hashes
|
|
34
|
+
- Roadmap: removed stale Pagination entry from 0.8.0 (shipped in 0.3.0); Retry/backoff renumbered to 0.8.0
|
|
35
|
+
|
|
11
36
|
## [0.3.0] - 2026-03-29
|
|
12
37
|
|
|
13
38
|
### Added
|
data/docs/FUTURE.md
CHANGED
|
@@ -20,21 +20,6 @@ printavo sync orders --to crm
|
|
|
20
20
|
|
|
21
21
|
Planned version: `0.7.0`
|
|
22
22
|
|
|
23
|
-
### Pagination Abstraction
|
|
24
|
-
|
|
25
|
-
A lazy-enumerator-style helper to automatically page through all results:
|
|
26
|
-
|
|
27
|
-
```ruby
|
|
28
|
-
client.customers.each_page do |page|
|
|
29
|
-
page.each { |c| process(c) }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Or collect all:
|
|
33
|
-
all_orders = client.orders.all_pages
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Planned version: `0.8.0`
|
|
37
|
-
|
|
38
23
|
### Retry/Backoff
|
|
39
24
|
|
|
40
25
|
Intelligent rate limit handling with exponential backoff:
|
|
@@ -48,7 +33,7 @@ client = Printavo::Client.new(
|
|
|
48
33
|
)
|
|
49
34
|
```
|
|
50
35
|
|
|
51
|
-
Planned version: `0.
|
|
36
|
+
Planned version: `0.8.0`
|
|
52
37
|
|
|
53
38
|
### Analytics / Reporting Expansion
|
|
54
39
|
|
|
@@ -7,16 +7,90 @@ module Printavo
|
|
|
7
7
|
@connection = connection
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# Executes a GraphQL query and returns the parsed `data` hash.
|
|
11
|
+
#
|
|
12
|
+
# @param query_string [String] the GraphQL query document
|
|
13
|
+
# @param variables [Hash] optional input variables
|
|
14
|
+
# @return [Hash]
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# client.graphql.query("{ customers { nodes { id } } }")
|
|
18
|
+
# client.graphql.query(
|
|
19
|
+
# "query Customer($id: ID!) { customer(id: $id) { id email } }",
|
|
20
|
+
# variables: { id: "42" }
|
|
21
|
+
# )
|
|
10
22
|
def query(query_string, variables: {})
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
23
|
+
execute(query_string, variables: variables)
|
|
24
|
+
end
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
# Executes a GraphQL mutation and returns the parsed `data` hash.
|
|
27
|
+
# Semantically equivalent to `query` — both POST to the same endpoint —
|
|
28
|
+
# but distinguishes write intent at the call site.
|
|
29
|
+
#
|
|
30
|
+
# @param mutation_string [String] the GraphQL mutation document
|
|
31
|
+
# @param variables [Hash] optional input variables
|
|
32
|
+
# @return [Hash]
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# client.graphql.mutate(
|
|
36
|
+
# <<~GQL,
|
|
37
|
+
# mutation UpdateOrder($id: ID!, $input: OrderInput!) {
|
|
38
|
+
# updateOrder(id: $id, input: $input) {
|
|
39
|
+
# order { id nickname }
|
|
40
|
+
# errors
|
|
41
|
+
# }
|
|
42
|
+
# }
|
|
43
|
+
# GQL
|
|
44
|
+
# variables: { id: "99", input: { nickname: "Rush Job" } }
|
|
45
|
+
# )
|
|
46
|
+
def mutate(mutation_string, variables: {})
|
|
47
|
+
execute(mutation_string, variables: variables)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Iterates all pages of a paginated GraphQL query, yielding each page's
|
|
51
|
+
# nodes array. The query must accept `$first: Int` and `$after: String`
|
|
52
|
+
# variables, and the target connection must expose `nodes` and `pageInfo`.
|
|
53
|
+
#
|
|
54
|
+
# @param query_string [String] the GraphQL query document
|
|
55
|
+
# @param path [String] dot-separated key path to the connection in the response
|
|
56
|
+
# e.g. "orders" or "customer.orders"
|
|
57
|
+
# @param variables [Hash] additional variables merged with `first` and `after`
|
|
58
|
+
# @param first [Integer] page size (default 25)
|
|
59
|
+
# @yieldparam nodes [Array<Hash>] one page of raw node hashes
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# client.graphql.paginate(ORDERS_QUERY, path: "orders") do |nodes|
|
|
63
|
+
# nodes.each { |n| puts n["nickname"] }
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# @example With extra variables
|
|
67
|
+
# client.graphql.paginate(JOBS_QUERY, path: "order.lineItems",
|
|
68
|
+
# variables: { orderId: "99" }, first: 50) do |nodes|
|
|
69
|
+
# nodes.each { |j| puts j["name"] }
|
|
70
|
+
# end
|
|
71
|
+
def paginate(query_string, path:, variables: {}, first: 25)
|
|
72
|
+
after = nil
|
|
73
|
+
loop do
|
|
74
|
+
data = execute(query_string, variables: variables.merge(first: first, after: after))
|
|
75
|
+
conn = dig_path(data, path)
|
|
76
|
+
nodes = conn&.fetch('nodes', []) || []
|
|
77
|
+
yield nodes
|
|
78
|
+
page_info = conn&.fetch('pageInfo', {}) || {}
|
|
79
|
+
break unless page_info['hasNextPage']
|
|
80
|
+
|
|
81
|
+
after = page_info['endCursor']
|
|
82
|
+
end
|
|
16
83
|
end
|
|
17
84
|
|
|
18
85
|
private
|
|
19
86
|
|
|
87
|
+
def execute(document, variables: {})
|
|
88
|
+
response = @connection.post('') do |req|
|
|
89
|
+
req.body = JSON.generate(query: document, variables: variables)
|
|
90
|
+
end
|
|
91
|
+
handle_response(response)
|
|
92
|
+
end
|
|
93
|
+
|
|
20
94
|
def handle_response(response)
|
|
21
95
|
body = response.body
|
|
22
96
|
|
|
@@ -34,5 +108,11 @@ module Printavo
|
|
|
34
108
|
|
|
35
109
|
body.is_a?(Hash) ? body['data'] : body
|
|
36
110
|
end
|
|
111
|
+
|
|
112
|
+
# Resolves a dot-separated path against a nested hash.
|
|
113
|
+
# e.g. dig_path(data, "customer.orders") => data["customer"]["orders"]
|
|
114
|
+
def dig_path(data, path)
|
|
115
|
+
path.split('.').reduce(data) { |obj, key| obj.is_a?(Hash) ? obj[key] : nil }
|
|
116
|
+
end
|
|
37
117
|
end
|
|
38
118
|
end
|
|
@@ -34,6 +34,38 @@ module Printavo
|
|
|
34
34
|
}
|
|
35
35
|
GQL
|
|
36
36
|
|
|
37
|
+
CREATE_MUTATION = <<~GQL.freeze
|
|
38
|
+
mutation CustomerCreate($input: CustomerCreateInput!) {
|
|
39
|
+
customerCreate(input: $input) {
|
|
40
|
+
id
|
|
41
|
+
companyName
|
|
42
|
+
primaryContact {
|
|
43
|
+
id
|
|
44
|
+
firstName
|
|
45
|
+
lastName
|
|
46
|
+
email
|
|
47
|
+
phone
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
GQL
|
|
52
|
+
|
|
53
|
+
UPDATE_MUTATION = <<~GQL.freeze
|
|
54
|
+
mutation CustomerUpdate($id: ID!, $input: CustomerInput!) {
|
|
55
|
+
customerUpdate(id: $id, input: $input) {
|
|
56
|
+
id
|
|
57
|
+
companyName
|
|
58
|
+
primaryContact {
|
|
59
|
+
id
|
|
60
|
+
firstName
|
|
61
|
+
lastName
|
|
62
|
+
email
|
|
63
|
+
phone
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
GQL
|
|
68
|
+
|
|
37
69
|
def all(first: 25, after: nil)
|
|
38
70
|
fetch_page(first: first, after: after).records
|
|
39
71
|
end
|
|
@@ -43,6 +75,36 @@ module Printavo
|
|
|
43
75
|
Printavo::Customer.new(data['customer'])
|
|
44
76
|
end
|
|
45
77
|
|
|
78
|
+
# Creates a new customer. Requires a +primary_contact+ hash with at least
|
|
79
|
+
# +firstName+ and +email+. Optional keyword arguments map to CustomerCreateInput.
|
|
80
|
+
#
|
|
81
|
+
# @param primary_contact [Hash] contact fields (firstName, lastName, email, phone)
|
|
82
|
+
# @return [Printavo::Customer]
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# client.customers.create(
|
|
86
|
+
# primary_contact: { firstName: "Jane", lastName: "Smith", email: "jane@example.com" },
|
|
87
|
+
# company_name: "Acme Shirts"
|
|
88
|
+
# )
|
|
89
|
+
def create(primary_contact:, **input)
|
|
90
|
+
variables = { input: camelize_keys(input).merge(primaryContact: primary_contact) }
|
|
91
|
+
data = @graphql.mutate(CREATE_MUTATION, variables: variables)
|
|
92
|
+
build_customer(data['customerCreate'])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Updates an existing customer by ID.
|
|
96
|
+
#
|
|
97
|
+
# @param id [String, Integer]
|
|
98
|
+
# @return [Printavo::Customer]
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# client.customers.update("42", company_name: "New Name")
|
|
102
|
+
def update(id, **input)
|
|
103
|
+
data = @graphql.mutate(UPDATE_MUTATION,
|
|
104
|
+
variables: { id: id.to_s, input: camelize_keys(input) })
|
|
105
|
+
build_customer(data['customerUpdate'])
|
|
106
|
+
end
|
|
107
|
+
|
|
46
108
|
private
|
|
47
109
|
|
|
48
110
|
def fetch_page(first: 25, after: nil, **)
|
|
@@ -55,6 +117,29 @@ module Printavo
|
|
|
55
117
|
end_cursor: page_info['endCursor']
|
|
56
118
|
)
|
|
57
119
|
end
|
|
120
|
+
|
|
121
|
+
# Normalizes a mutation response into the shape Customer.new expects.
|
|
122
|
+
# The mutation returns companyName + nested primaryContact; our model
|
|
123
|
+
# reads company, firstName, lastName, email, phone from the top level.
|
|
124
|
+
def build_customer(attrs)
|
|
125
|
+
return nil unless attrs
|
|
126
|
+
|
|
127
|
+
normalized = attrs.dup
|
|
128
|
+
normalized['company'] ||= attrs['companyName']
|
|
129
|
+
if (contact = attrs['primaryContact'])
|
|
130
|
+
%w[firstName lastName email phone].each do |field|
|
|
131
|
+
normalized[field] ||= contact[field]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
Printavo::Customer.new(normalized)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Converts snake_case Ruby keyword args to camelCase for GraphQL input.
|
|
138
|
+
def camelize_keys(hash)
|
|
139
|
+
hash.transform_keys do |key|
|
|
140
|
+
key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
58
143
|
end
|
|
59
144
|
end
|
|
60
145
|
end
|
|
@@ -52,6 +52,50 @@ module Printavo
|
|
|
52
52
|
}
|
|
53
53
|
GQL
|
|
54
54
|
|
|
55
|
+
CREATE_MUTATION = <<~GQL.freeze
|
|
56
|
+
mutation InquiryCreate($input: InquiryCreateInput!) {
|
|
57
|
+
inquiryCreate(input: $input) {
|
|
58
|
+
id
|
|
59
|
+
nickname
|
|
60
|
+
totalPrice
|
|
61
|
+
status {
|
|
62
|
+
id
|
|
63
|
+
name
|
|
64
|
+
color
|
|
65
|
+
}
|
|
66
|
+
customer {
|
|
67
|
+
id
|
|
68
|
+
firstName
|
|
69
|
+
lastName
|
|
70
|
+
email
|
|
71
|
+
company
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
GQL
|
|
76
|
+
|
|
77
|
+
UPDATE_MUTATION = <<~GQL.freeze
|
|
78
|
+
mutation InquiryUpdate($id: ID!, $input: InquiryInput!) {
|
|
79
|
+
inquiryUpdate(id: $id, input: $input) {
|
|
80
|
+
id
|
|
81
|
+
nickname
|
|
82
|
+
totalPrice
|
|
83
|
+
status {
|
|
84
|
+
id
|
|
85
|
+
name
|
|
86
|
+
color
|
|
87
|
+
}
|
|
88
|
+
customer {
|
|
89
|
+
id
|
|
90
|
+
firstName
|
|
91
|
+
lastName
|
|
92
|
+
email
|
|
93
|
+
company
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
GQL
|
|
98
|
+
|
|
55
99
|
def all(first: 25, after: nil)
|
|
56
100
|
fetch_page(first: first, after: after).records
|
|
57
101
|
end
|
|
@@ -61,6 +105,31 @@ module Printavo
|
|
|
61
105
|
Printavo::Inquiry.new(data['inquiry'])
|
|
62
106
|
end
|
|
63
107
|
|
|
108
|
+
# Creates a new inquiry. Requires +name:+ at minimum.
|
|
109
|
+
#
|
|
110
|
+
# @return [Printavo::Inquiry]
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# client.inquiries.create(name: "Jane Smith", email: "jane@example.com",
|
|
114
|
+
# request: "100 hoodies, front + back print")
|
|
115
|
+
def create(**input)
|
|
116
|
+
data = @graphql.mutate(CREATE_MUTATION, variables: { input: input })
|
|
117
|
+
Printavo::Inquiry.new(data['inquiryCreate'])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Updates an existing inquiry by ID.
|
|
121
|
+
#
|
|
122
|
+
# @param id [String, Integer]
|
|
123
|
+
# @return [Printavo::Inquiry]
|
|
124
|
+
#
|
|
125
|
+
# @example
|
|
126
|
+
# client.inquiries.update("55", nickname: "Hoodies Rush")
|
|
127
|
+
def update(id, **input)
|
|
128
|
+
data = @graphql.mutate(UPDATE_MUTATION,
|
|
129
|
+
variables: { id: id.to_s, input: input })
|
|
130
|
+
Printavo::Inquiry.new(data['inquiryUpdate'])
|
|
131
|
+
end
|
|
132
|
+
|
|
64
133
|
private
|
|
65
134
|
|
|
66
135
|
def fetch_page(first: 25, after: nil, **)
|
|
@@ -52,6 +52,73 @@ module Printavo
|
|
|
52
52
|
}
|
|
53
53
|
GQL
|
|
54
54
|
|
|
55
|
+
# Printavo creates orders as quotes first; the mutation is quoteCreate.
|
|
56
|
+
CREATE_MUTATION = <<~GQL.freeze
|
|
57
|
+
mutation QuoteCreate($input: QuoteCreateInput!) {
|
|
58
|
+
quoteCreate(input: $input) {
|
|
59
|
+
id
|
|
60
|
+
nickname
|
|
61
|
+
total
|
|
62
|
+
status {
|
|
63
|
+
id
|
|
64
|
+
name
|
|
65
|
+
color
|
|
66
|
+
}
|
|
67
|
+
customer {
|
|
68
|
+
id
|
|
69
|
+
firstName
|
|
70
|
+
lastName
|
|
71
|
+
email
|
|
72
|
+
company
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
GQL
|
|
77
|
+
|
|
78
|
+
UPDATE_MUTATION = <<~GQL.freeze
|
|
79
|
+
mutation QuoteUpdate($id: ID!, $input: QuoteInput!) {
|
|
80
|
+
quoteUpdate(id: $id, input: $input) {
|
|
81
|
+
id
|
|
82
|
+
nickname
|
|
83
|
+
total
|
|
84
|
+
status {
|
|
85
|
+
id
|
|
86
|
+
name
|
|
87
|
+
color
|
|
88
|
+
}
|
|
89
|
+
customer {
|
|
90
|
+
id
|
|
91
|
+
firstName
|
|
92
|
+
lastName
|
|
93
|
+
email
|
|
94
|
+
company
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
GQL
|
|
99
|
+
|
|
100
|
+
# statusUpdate returns an OrderUnion (Quote | Invoice) — requires fragments.
|
|
101
|
+
UPDATE_STATUS_MUTATION = <<~GQL.freeze
|
|
102
|
+
mutation StatusUpdate($parentId: ID!, $statusId: ID!) {
|
|
103
|
+
statusUpdate(parentId: $parentId, statusId: $statusId) {
|
|
104
|
+
... on Quote {
|
|
105
|
+
id
|
|
106
|
+
nickname
|
|
107
|
+
total
|
|
108
|
+
status { id name color }
|
|
109
|
+
customer { id firstName lastName email company }
|
|
110
|
+
}
|
|
111
|
+
... on Invoice {
|
|
112
|
+
id
|
|
113
|
+
nickname
|
|
114
|
+
total
|
|
115
|
+
status { id name color }
|
|
116
|
+
customer { id firstName lastName email company }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
GQL
|
|
121
|
+
|
|
55
122
|
def all(first: 25, after: nil)
|
|
56
123
|
fetch_page(first: first, after: after).records
|
|
57
124
|
end
|
|
@@ -61,6 +128,50 @@ module Printavo
|
|
|
61
128
|
Printavo::Order.new(data['order'])
|
|
62
129
|
end
|
|
63
130
|
|
|
131
|
+
# Creates a new order (quote) in Printavo.
|
|
132
|
+
# Requires at minimum +contact:+ (IDInput), +due_at:+, and +customer_due_at:+.
|
|
133
|
+
#
|
|
134
|
+
# @return [Printavo::Order]
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# client.orders.create(
|
|
138
|
+
# contact: { id: "456" },
|
|
139
|
+
# due_at: "2026-06-01T09:00:00Z",
|
|
140
|
+
# customer_due_at: "2026-06-01"
|
|
141
|
+
# )
|
|
142
|
+
def create(**input)
|
|
143
|
+
data = @graphql.mutate(CREATE_MUTATION, variables: { input: camelize_keys(input) })
|
|
144
|
+
build_order(data['quoteCreate'])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Updates an existing order (quote or invoice) by ID.
|
|
148
|
+
#
|
|
149
|
+
# @param id [String, Integer]
|
|
150
|
+
# @return [Printavo::Order]
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# client.orders.update("99", nickname: "Rush Job", production_note: "Ships Friday")
|
|
154
|
+
def update(id, **input)
|
|
155
|
+
data = @graphql.mutate(UPDATE_MUTATION,
|
|
156
|
+
variables: { id: id.to_s, input: camelize_keys(input) })
|
|
157
|
+
build_order(data['quoteUpdate'])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Moves an order to a new status.
|
|
161
|
+
#
|
|
162
|
+
# @param id [String, Integer] order (quote/invoice) ID
|
|
163
|
+
# @param status_id [String, Integer] target status ID
|
|
164
|
+
# @return [Printavo::Order]
|
|
165
|
+
#
|
|
166
|
+
# @example
|
|
167
|
+
# registry = client.statuses.registry
|
|
168
|
+
# client.orders.update_status("99", status_id: registry[:in_production].id)
|
|
169
|
+
def update_status(id, status_id:)
|
|
170
|
+
data = @graphql.mutate(UPDATE_STATUS_MUTATION,
|
|
171
|
+
variables: { parentId: id.to_s, statusId: status_id.to_s })
|
|
172
|
+
build_order(data['statusUpdate'])
|
|
173
|
+
end
|
|
174
|
+
|
|
64
175
|
private
|
|
65
176
|
|
|
66
177
|
def fetch_page(first: 25, after: nil, **)
|
|
@@ -73,6 +184,22 @@ module Printavo
|
|
|
73
184
|
end_cursor: page_info['endCursor']
|
|
74
185
|
)
|
|
75
186
|
end
|
|
187
|
+
|
|
188
|
+
# Normalizes mutation response: quoteCreate/quoteUpdate return `total`
|
|
189
|
+
# rather than `totalPrice`. Map it so Order model accessors work unchanged.
|
|
190
|
+
def build_order(attrs)
|
|
191
|
+
return nil unless attrs
|
|
192
|
+
|
|
193
|
+
normalized = attrs.dup
|
|
194
|
+
normalized['totalPrice'] ||= attrs['total']
|
|
195
|
+
Printavo::Order.new(normalized)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def camelize_keys(hash)
|
|
199
|
+
hash.transform_keys do |key|
|
|
200
|
+
key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
76
203
|
end
|
|
77
204
|
end
|
|
78
205
|
end
|
data/lib/printavo/version.rb
CHANGED