printavo-ruby 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bdc16c358a3dafbdae1842e0ddb8eebbe8775053b1b95efb2308cd33056e2ea
4
- data.tar.gz: 7399f57922f9244a02900c458bf9d2b9830aa0d63a5b4ff07952ca3ade6254fd
3
+ metadata.gz: 5c8061b68869c47ca86f6c37dc51d60020c035364ea70c68e68bad1df2f4da6c
4
+ data.tar.gz: 1d9c8f4d5805ab82bca17f28891fdc21899f2b166638c4e100e8531c8275073f
5
5
  SHA512:
6
- metadata.gz: fa26e26063d2ed94011d20f22c4ae6c9fa964c62d2fa07b31343b97f2d1e9dbe623114960dab489cca029fe0d1f036964c7748b27a96e1c2eb7d63d33d3d285f
7
- data.tar.gz: 0427ed5bc1601858080cb1f057547cb4143cc51d32cbd7189fa4fc8546260045c11603204814e28656b26a97f00e286634b24f8b536c879d14e5f8dd44f8313c
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
@@ -294,7 +345,7 @@ end
294
345
  | 0.2.0 | Status registry + Inquiries ✅ |
295
346
  | 0.3.0 | Pagination helpers (`each_page`, `all_pages`) + `bin/lint` ✅ |
296
347
  | 0.4.0 | Expanded GraphQL DSL (`mutate`, `paginate`) ✅ |
297
- | 0.5.0 | Mutations (create/update) |
348
+ | 0.5.0 | Mutations (create/update) |
298
349
  | 0.6.0 | Analytics / Reporting queries |
299
350
  | 0.7.0 | Community burn-in / API stabilization |
300
351
  | 0.8.0 | Retry/backoff + rate limit awareness |
data/docs/CHANGELOG.md CHANGED
@@ -8,6 +8,22 @@ 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
+
11
27
  ## [0.4.0] - 2026-03-30
12
28
 
13
29
  ### Added
@@ -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
@@ -1,4 +1,4 @@
1
1
  # lib/printavo/version.rb
2
2
  module Printavo
3
- VERSION = '0.4.0'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: printavo-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Carver II