printavo-ruby 0.4.0 → 0.5.1

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: 34f4d971a1f4d0c685fb6780bc12d7737c915d533bc49c9d96fb5c73522eec58
4
+ data.tar.gz: 687c158df7fa205ac7aa5607bb2c6a7171ee7eac972005b0b5d102cee3b7dd0d
5
5
  SHA512:
6
- metadata.gz: fa26e26063d2ed94011d20f22c4ae6c9fa964c62d2fa07b31343b97f2d1e9dbe623114960dab489cca029fe0d1f036964c7748b27a96e1c2eb7d63d33d3d285f
7
- data.tar.gz: 0427ed5bc1601858080cb1f057547cb4143cc51d32cbd7189fa4fc8546260045c11603204814e28656b26a97f00e286634b24f8b536c879d14e5f8dd44f8313c
6
+ metadata.gz: 402856b46761b8a924bd92a5e433d99faf0f5aa976b9e7e9e4a2e35ee814dda18e4caddafe40f890507832535e233596e6979a439a26895bf20a8caefe8979ad
7
+ data.tar.gz: dd48f4059d8d5d596e1a3e569cdecaa3842c79713ea2da48c112c86664c42f418c035988bbc9c11e48e337508e6af6aa8f6c9938bacf762eda79a5b2e6366e76
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
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/client.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Client
4
6
  attr_reader :graphql
@@ -1,9 +1,11 @@
1
1
  # lib/printavo/config.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Config
4
6
  attr_accessor :email, :token, :base_url, :timeout
5
7
 
6
- BASE_URL = 'https://www.printavo.com/api/v2'.freeze
8
+ BASE_URL = 'https://www.printavo.com/api/v2'
7
9
 
8
10
  def initialize
9
11
  @base_url = BASE_URL
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/connection.rb
2
+ # frozen_string_literal: true
3
+
2
4
  require 'faraday'
3
5
  require 'faraday/retry'
4
6
 
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/errors.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Error < StandardError; end
4
6
 
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/graphql_client.rb
2
+ # frozen_string_literal: true
3
+
2
4
  require 'json'
3
5
 
4
6
  module Printavo
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/base.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Models
4
6
  class Base
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/customer.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Customer < Models::Base
4
6
  def id = self['id']
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/inquiry.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Inquiry < Models::Base
4
6
  def id = self['id']
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/job.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Job < Models::Base
4
6
  def id = self['id']
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/order.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Order < Models::Base
4
6
  def id = self['id']
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/models/status.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  class Status < Models::Base
4
6
  def id = self['id']
data/lib/printavo/page.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/page.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  # Wraps a single page of API results with cursor metadata.
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/resources/base.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Base
@@ -1,38 +1,13 @@
1
1
  # lib/printavo/resources/customers.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Customers < Base
5
- ALL_QUERY = <<~GQL.freeze
6
- query Customers($first: Int, $after: String) {
7
- customers(first: $first, after: $after) {
8
- nodes {
9
- id
10
- firstName
11
- lastName
12
- email
13
- phone
14
- company
15
- }
16
- pageInfo {
17
- hasNextPage
18
- endCursor
19
- }
20
- }
21
- }
22
- GQL
23
-
24
- FIND_QUERY = <<~GQL.freeze
25
- query Customer($id: ID!) {
26
- customer(id: $id) {
27
- id
28
- firstName
29
- lastName
30
- email
31
- phone
32
- company
33
- }
34
- }
35
- GQL
7
+ ALL_QUERY = File.read(File.join(__dir__, '../graphql/customers/all.graphql')).freeze
8
+ FIND_QUERY = File.read(File.join(__dir__, '../graphql/customers/find.graphql')).freeze
9
+ CREATE_MUTATION = File.read(File.join(__dir__, '../graphql/customers/create.graphql')).freeze
10
+ UPDATE_MUTATION = File.read(File.join(__dir__, '../graphql/customers/update.graphql')).freeze
36
11
 
37
12
  def all(first: 25, after: nil)
38
13
  fetch_page(first: first, after: after).records
@@ -43,6 +18,36 @@ module Printavo
43
18
  Printavo::Customer.new(data['customer'])
44
19
  end
45
20
 
21
+ # Creates a new customer. Requires a +primary_contact+ hash with at least
22
+ # +firstName+ and +email+. Optional keyword arguments map to CustomerCreateInput.
23
+ #
24
+ # @param primary_contact [Hash] contact fields (firstName, lastName, email, phone)
25
+ # @return [Printavo::Customer]
26
+ #
27
+ # @example
28
+ # client.customers.create(
29
+ # primary_contact: { firstName: "Jane", lastName: "Smith", email: "jane@example.com" },
30
+ # company_name: "Acme Shirts"
31
+ # )
32
+ def create(primary_contact:, **input)
33
+ variables = { input: camelize_keys(input).merge(primaryContact: primary_contact) }
34
+ data = @graphql.mutate(CREATE_MUTATION, variables: variables)
35
+ build_customer(data['customerCreate'])
36
+ end
37
+
38
+ # Updates an existing customer by ID.
39
+ #
40
+ # @param id [String, Integer]
41
+ # @return [Printavo::Customer]
42
+ #
43
+ # @example
44
+ # client.customers.update("42", company_name: "New Name")
45
+ def update(id, **input)
46
+ data = @graphql.mutate(UPDATE_MUTATION,
47
+ variables: { id: id.to_s, input: camelize_keys(input) })
48
+ build_customer(data['customerUpdate'])
49
+ end
50
+
46
51
  private
47
52
 
48
53
  def fetch_page(first: 25, after: nil, **)
@@ -55,6 +60,29 @@ module Printavo
55
60
  end_cursor: page_info['endCursor']
56
61
  )
57
62
  end
63
+
64
+ # Normalizes a mutation response into the shape Customer.new expects.
65
+ # The mutation returns companyName + nested primaryContact; our model
66
+ # reads company, firstName, lastName, email, phone from the top level.
67
+ def build_customer(attrs)
68
+ return nil unless attrs
69
+
70
+ normalized = attrs.dup
71
+ normalized['company'] ||= attrs['companyName']
72
+ if (contact = attrs['primaryContact'])
73
+ %w[firstName lastName email phone].each do |field|
74
+ normalized[field] ||= contact[field]
75
+ end
76
+ end
77
+ Printavo::Customer.new(normalized)
78
+ end
79
+
80
+ # Converts snake_case Ruby keyword args to camelCase for GraphQL input.
81
+ def camelize_keys(hash)
82
+ hash.transform_keys do |key|
83
+ key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
84
+ end
85
+ end
58
86
  end
59
87
  end
60
88
  end
@@ -1,56 +1,13 @@
1
1
  # lib/printavo/resources/inquiries.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Inquiries < Base
5
- ALL_QUERY = <<~GQL.freeze
6
- query Inquiries($first: Int, $after: String) {
7
- inquiries(first: $first, after: $after) {
8
- nodes {
9
- id
10
- nickname
11
- totalPrice
12
- status {
13
- id
14
- name
15
- color
16
- }
17
- customer {
18
- id
19
- firstName
20
- lastName
21
- email
22
- company
23
- }
24
- }
25
- pageInfo {
26
- hasNextPage
27
- endCursor
28
- }
29
- }
30
- }
31
- GQL
32
-
33
- FIND_QUERY = <<~GQL.freeze
34
- query Inquiry($id: ID!) {
35
- inquiry(id: $id) {
36
- id
37
- nickname
38
- totalPrice
39
- status {
40
- id
41
- name
42
- color
43
- }
44
- customer {
45
- id
46
- firstName
47
- lastName
48
- email
49
- company
50
- }
51
- }
52
- }
53
- GQL
7
+ ALL_QUERY = File.read(File.join(__dir__, '../graphql/inquiries/all.graphql')).freeze
8
+ FIND_QUERY = File.read(File.join(__dir__, '../graphql/inquiries/find.graphql')).freeze
9
+ CREATE_MUTATION = File.read(File.join(__dir__, '../graphql/inquiries/create.graphql')).freeze
10
+ UPDATE_MUTATION = File.read(File.join(__dir__, '../graphql/inquiries/update.graphql')).freeze
54
11
 
55
12
  def all(first: 25, after: nil)
56
13
  fetch_page(first: first, after: after).records
@@ -61,6 +18,31 @@ module Printavo
61
18
  Printavo::Inquiry.new(data['inquiry'])
62
19
  end
63
20
 
21
+ # Creates a new inquiry. Requires +name:+ at minimum.
22
+ #
23
+ # @return [Printavo::Inquiry]
24
+ #
25
+ # @example
26
+ # client.inquiries.create(name: "Jane Smith", email: "jane@example.com",
27
+ # request: "100 hoodies, front + back print")
28
+ def create(**input)
29
+ data = @graphql.mutate(CREATE_MUTATION, variables: { input: input })
30
+ Printavo::Inquiry.new(data['inquiryCreate'])
31
+ end
32
+
33
+ # Updates an existing inquiry by ID.
34
+ #
35
+ # @param id [String, Integer]
36
+ # @return [Printavo::Inquiry]
37
+ #
38
+ # @example
39
+ # client.inquiries.update("55", nickname: "Hoodies Rush")
40
+ def update(id, **input)
41
+ data = @graphql.mutate(UPDATE_MUTATION,
42
+ variables: { id: id.to_s, input: input })
43
+ Printavo::Inquiry.new(data['inquiryUpdate'])
44
+ end
45
+
64
46
  private
65
47
 
66
48
  def fetch_page(first: 25, after: nil, **)
@@ -1,38 +1,11 @@
1
1
  # lib/printavo/resources/jobs.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Jobs < Base
5
- ALL_QUERY = <<~GQL.freeze
6
- query LineItems($orderId: ID!, $first: Int, $after: String) {
7
- order(id: $orderId) {
8
- lineItems(first: $first, after: $after) {
9
- nodes {
10
- id
11
- name
12
- quantity
13
- price
14
- taxable
15
- }
16
- pageInfo {
17
- hasNextPage
18
- endCursor
19
- }
20
- }
21
- }
22
- }
23
- GQL
24
-
25
- FIND_QUERY = <<~GQL.freeze
26
- query LineItem($id: ID!) {
27
- lineItem(id: $id) {
28
- id
29
- name
30
- quantity
31
- price
32
- taxable
33
- }
34
- }
35
- GQL
7
+ ALL_QUERY = File.read(File.join(__dir__, '../graphql/jobs/all.graphql')).freeze
8
+ FIND_QUERY = File.read(File.join(__dir__, '../graphql/jobs/find.graphql')).freeze
36
9
 
37
10
  def all(order_id:, first: 25, after: nil)
38
11
  fetch_page(order_id: order_id, first: first, after: after).records
@@ -1,56 +1,16 @@
1
1
  # lib/printavo/resources/orders.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Orders < Base
5
- ALL_QUERY = <<~GQL.freeze
6
- query Orders($first: Int, $after: String) {
7
- orders(first: $first, after: $after) {
8
- nodes {
9
- id
10
- nickname
11
- totalPrice
12
- status {
13
- id
14
- name
15
- color
16
- }
17
- customer {
18
- id
19
- firstName
20
- lastName
21
- email
22
- company
23
- }
24
- }
25
- pageInfo {
26
- hasNextPage
27
- endCursor
28
- }
29
- }
30
- }
31
- GQL
32
-
33
- FIND_QUERY = <<~GQL.freeze
34
- query Order($id: ID!) {
35
- order(id: $id) {
36
- id
37
- nickname
38
- totalPrice
39
- status {
40
- id
41
- name
42
- color
43
- }
44
- customer {
45
- id
46
- firstName
47
- lastName
48
- email
49
- company
50
- }
51
- }
52
- }
53
- GQL
7
+ ALL_QUERY = File.read(File.join(__dir__, '../graphql/orders/all.graphql')).freeze
8
+ FIND_QUERY = File.read(File.join(__dir__, '../graphql/orders/find.graphql')).freeze
9
+ # Printavo creates orders as quotes first; the mutation is quoteCreate.
10
+ CREATE_MUTATION = File.read(File.join(__dir__, '../graphql/orders/create.graphql')).freeze
11
+ UPDATE_MUTATION = File.read(File.join(__dir__, '../graphql/orders/update.graphql')).freeze
12
+ # statusUpdate returns an OrderUnion (Quote | Invoice) — requires fragments.
13
+ UPDATE_STATUS_MUTATION = File.read(File.join(__dir__, '../graphql/orders/update_status.graphql')).freeze
54
14
 
55
15
  def all(first: 25, after: nil)
56
16
  fetch_page(first: first, after: after).records
@@ -61,6 +21,50 @@ module Printavo
61
21
  Printavo::Order.new(data['order'])
62
22
  end
63
23
 
24
+ # Creates a new order (quote) in Printavo.
25
+ # Requires at minimum +contact:+ (IDInput), +due_at:+, and +customer_due_at:+.
26
+ #
27
+ # @return [Printavo::Order]
28
+ #
29
+ # @example
30
+ # client.orders.create(
31
+ # contact: { id: "456" },
32
+ # due_at: "2026-06-01T09:00:00Z",
33
+ # customer_due_at: "2026-06-01"
34
+ # )
35
+ def create(**input)
36
+ data = @graphql.mutate(CREATE_MUTATION, variables: { input: camelize_keys(input) })
37
+ build_order(data['quoteCreate'])
38
+ end
39
+
40
+ # Updates an existing order (quote or invoice) by ID.
41
+ #
42
+ # @param id [String, Integer]
43
+ # @return [Printavo::Order]
44
+ #
45
+ # @example
46
+ # client.orders.update("99", nickname: "Rush Job", production_note: "Ships Friday")
47
+ def update(id, **input)
48
+ data = @graphql.mutate(UPDATE_MUTATION,
49
+ variables: { id: id.to_s, input: camelize_keys(input) })
50
+ build_order(data['quoteUpdate'])
51
+ end
52
+
53
+ # Moves an order to a new status.
54
+ #
55
+ # @param id [String, Integer] order (quote/invoice) ID
56
+ # @param status_id [String, Integer] target status ID
57
+ # @return [Printavo::Order]
58
+ #
59
+ # @example
60
+ # registry = client.statuses.registry
61
+ # client.orders.update_status("99", status_id: registry[:in_production].id)
62
+ def update_status(id, status_id:)
63
+ data = @graphql.mutate(UPDATE_STATUS_MUTATION,
64
+ variables: { parentId: id.to_s, statusId: status_id.to_s })
65
+ build_order(data['statusUpdate'])
66
+ end
67
+
64
68
  private
65
69
 
66
70
  def fetch_page(first: 25, after: nil, **)
@@ -73,6 +77,22 @@ module Printavo
73
77
  end_cursor: page_info['endCursor']
74
78
  )
75
79
  end
80
+
81
+ # Normalizes mutation response: quoteCreate/quoteUpdate return `total`
82
+ # rather than `totalPrice`. Map it so Order model accessors work unchanged.
83
+ def build_order(attrs)
84
+ return nil unless attrs
85
+
86
+ normalized = attrs.dup
87
+ normalized['totalPrice'] ||= attrs['total']
88
+ Printavo::Order.new(normalized)
89
+ end
90
+
91
+ def camelize_keys(hash)
92
+ hash.transform_keys do |key|
93
+ key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
94
+ end
95
+ end
76
96
  end
77
97
  end
78
98
  end
@@ -1,32 +1,11 @@
1
1
  # lib/printavo/resources/statuses.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
5
  module Resources
4
6
  class Statuses < Base
5
- ALL_QUERY = <<~GQL.freeze
6
- query Statuses($first: Int, $after: String) {
7
- statuses(first: $first, after: $after) {
8
- nodes {
9
- id
10
- name
11
- color
12
- }
13
- pageInfo {
14
- hasNextPage
15
- endCursor
16
- }
17
- }
18
- }
19
- GQL
20
-
21
- FIND_QUERY = <<~GQL.freeze
22
- query Status($id: ID!) {
23
- status(id: $id) {
24
- id
25
- name
26
- color
27
- }
28
- }
29
- GQL
7
+ ALL_QUERY = File.read(File.join(__dir__, '../graphql/statuses/all.graphql')).freeze
8
+ FIND_QUERY = File.read(File.join(__dir__, '../graphql/statuses/find.graphql')).freeze
30
9
 
31
10
  def all(first: 100, after: nil)
32
11
  fetch_page(first: first, after: after).records
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/version.rb
2
+ # frozen_string_literal: true
3
+
2
4
  module Printavo
3
- VERSION = '0.4.0'.freeze
5
+ VERSION = '0.5.1'
4
6
  end
@@ -1,4 +1,6 @@
1
1
  # lib/printavo/webhooks.rb
2
+ # frozen_string_literal: true
3
+
2
4
  require 'openssl'
3
5
 
4
6
  module Printavo
data/lib/printavo.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # lib/printavo.rb
2
+ # frozen_string_literal: true
3
+
2
4
  require 'faraday'
3
5
  require 'faraday/retry'
4
6
  require 'json'
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Carver II