printavo-ruby 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f47615b00a6d63bc35e5d5cc74e5e8d591fe38218eadd0bb76e11798626f9f42
4
+ data.tar.gz: aa6d86cb12b1ea7617c8d97093993252fc30f1401fef8893263ed2ba4f277a48
5
+ SHA512:
6
+ metadata.gz: '08fa3eb45c8ddef4ff5962c9bd59d01c14048d2c68fefb1b3e6ee526b6e4317b225958b5ed8865ba45fb42b35ef14d1c8f235a81b90a3c11e1e4dd661e2c1aa4'
7
+ data.tar.gz: 93ef9341891c36ab6ffa9fd61a9790b16662072a7ecb4bf733d6aa0f5cd9fe99ee75c77a47be7a023909b7468fb70abec3a8d5b076925a732c3cd8c6389ec1b6
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ <!-- CHANGELOG.md -->
2
+ # Changelog
3
+
4
+ All notable changes to this project will be documented in this file.
5
+
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] - 2026-03-29
12
+
13
+ ### Added
14
+ - `Printavo::Client` — instance-based, multi-client capable entry point
15
+ - `Printavo::Resources::Customers` — `all` and `find` via GraphQL
16
+ - `Printavo::Resources::Orders` — `all` and `find` with status + customer association
17
+ - `Printavo::Resources::Jobs` — `all` (by order) and `find` line item queries
18
+ - `Printavo::Customer`, `Printavo::Order`, `Printavo::Job` — rich domain models
19
+ - `Printavo::Order#status?` — dynamic status predicate (handles user-defined statuses)
20
+ - `Printavo::GraphqlClient` — raw GraphQL query/mutation interface
21
+ - `Printavo::Webhooks.verify` — Rack-compatible HMAC-SHA256 signature verification
22
+ - Error hierarchy: `AuthenticationError`, `RateLimitError`, `NotFoundError`, `ApiError`
23
+ - Faraday connection with retry middleware (max 2 retries; 429/5xx)
24
+ - RSpec test suite — 62 examples, 100% line coverage with VCR + WebMock + Faker sanitization
25
+ - Coveralls coverage badge (LCOV via `simplecov-lcov`)
26
+ - Guard + RuboCop DX setup with `bin/spec` multi-Ruby local runner
27
+ - GitHub Actions CI: Ruby 3.3 (primary) + Ruby 4.0 (`continue-on-error`)
28
+ - Automated RubyGems publish on `v*` tag via `release.yml`
29
+ - `docs/CACHING.md` — nine caching strategy patterns for rate-limit-aware consumers
30
+
31
+ ---
32
+
33
+ — Stan Carver II / Made in Texas 🤠 / https://stancarver.com
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stan Carver II
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,252 @@
1
+ <!-- README.md -->
2
+
3
+ <p align="center">
4
+ <img src="docs/printavo-ruby.png" alt="printavo-ruby" width="640">
5
+ </p>
6
+
7
+ # printavo-ruby
8
+
9
+ [![CI](https://github.com/scarver2/printavo-ruby/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/scarver2/printavo-ruby/actions/workflows/ci.yml)
10
+ [![Coverage Status](https://coveralls.io/repos/github/scarver2/printavo-ruby/badge.svg?branch=master)](https://coveralls.io/github/scarver2/printavo-ruby?branch=master)
11
+ [![Gem Version](https://badge.fury.io/rb/printavo-ruby.svg)](https://badge.fury.io/rb/printavo-ruby)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
+
14
+ A framework-agnostic Ruby SDK for the [Printavo](https://www.printavo.com) GraphQL API (v2).
15
+
16
+ > I use Printavo every day at [Texas Embroidery Ranch](https://texasembroideryranch.com). This gem was created to bridge Printavo
17
+ > with other operational systems—CRM, marketing, finance, and automation—so that print shops
18
+ > can build integrated workflows without writing raw GraphQL by hand.
19
+
20
+ ## Features
21
+
22
+ - Full [Printavo v2 GraphQL API](https://www.printavo.com/docs/api/v2) support
23
+ - Resource-oriented interface: `client.customers.all`, `client.orders.find(id)`
24
+ - Raw GraphQL access: `client.graphql.query("{ ... }")`
25
+ - Rich domain models: `order.status`, `order.status?(:in_production)`, `order.customer`
26
+ - Rack-compatible webhook signature verification
27
+ - Multi-client support — no globals required
28
+ - Ruby 3.0+ required
29
+
30
+ ## Installation
31
+
32
+ Add to your Gemfile:
33
+
34
+ ```ruby
35
+ gem "printavo-ruby"
36
+ ```
37
+ or
38
+
39
+ ```bash
40
+ bundle add printavo-ruby
41
+ ```
42
+
43
+ or install directly:
44
+
45
+ ```bash
46
+ gem install printavo-ruby
47
+ ```
48
+
49
+ ## Authentication
50
+
51
+ Printavo authenticates via your account **email** and **API token**
52
+ (found at My Account → API Token on [printavo.com](https://www.printavo.com)).
53
+
54
+ ```ruby
55
+ require "printavo"
56
+
57
+ client = Printavo::Client.new(
58
+ email: ENV["PRINTAVO_EMAIL"],
59
+ token: ENV["PRINTAVO_TOKEN"]
60
+ )
61
+ ```
62
+
63
+ ### Rails Initializer
64
+
65
+ ```ruby
66
+ # config/initializers/printavo.rb
67
+ PRINTAVO = Printavo::Client.new(
68
+ email: ENV["PRINTAVO_EMAIL"],
69
+ token: ENV["PRINTAVO_TOKEN"]
70
+ )
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ### Customers
76
+
77
+ ```ruby
78
+ # List customers (25 per page by default)
79
+ customers = client.customers.all
80
+ customers.each { |c| puts "#{c.full_name} — #{c.email}" }
81
+
82
+ # Paginate
83
+ page_2 = client.customers.all(first: 25, after: cursor)
84
+
85
+ # Find a specific customer
86
+ customer = client.customers.find("12345")
87
+ puts customer.full_name # => "Jane Smith"
88
+ puts customer.company # => "Acme Shirts"
89
+ ```
90
+
91
+ ### Orders
92
+
93
+ ```ruby
94
+ # List orders
95
+ orders = client.orders.all
96
+ orders.each { |o| puts "#{o.nickname}: #{o.status}" }
97
+
98
+ # Find an order
99
+ order = client.orders.find("99")
100
+ puts order.status # => "In Production"
101
+ puts order.status_key # => :in_production
102
+ puts order.status?(:in_production) # => true
103
+ puts order.total_price # => "1250.00"
104
+ puts order.customer.full_name # => "Bob Johnson"
105
+ ```
106
+
107
+ ### Jobs (Line Items)
108
+
109
+ ```ruby
110
+ # List jobs for an order
111
+ jobs = client.jobs.all(order_id: "99")
112
+ jobs.each { |j| puts "#{j.name} x#{j.quantity} @ #{j.price}" }
113
+
114
+ # Find a specific job
115
+ job = client.jobs.find("77")
116
+ puts job.taxable? # => true
117
+ ```
118
+
119
+ ### Raw GraphQL
120
+
121
+ For queries not yet wrapped by a resource, use the raw GraphQL client directly:
122
+
123
+ ```ruby
124
+ result = client.graphql.query(<<~GQL)
125
+ {
126
+ customers(first: 5) {
127
+ nodes {
128
+ id
129
+ firstName
130
+ lastName
131
+ }
132
+ }
133
+ }
134
+ GQL
135
+
136
+ result["customers"]["nodes"].each { |c| puts c["firstName"] }
137
+ ```
138
+
139
+ With variables:
140
+
141
+ ```ruby
142
+ result = client.graphql.query(
143
+ "query Customer($id: ID!) { customer(id: $id) { id email } }",
144
+ variables: { id: "42" }
145
+ )
146
+ ```
147
+
148
+ ## Webhooks
149
+
150
+ `Printavo::Webhooks.verify` provides Rack-compatible HMAC-SHA256 signature verification.
151
+ No extra dependencies required.
152
+
153
+ ```ruby
154
+ # Pure Ruby / Rack
155
+ valid = Printavo::Webhooks.verify(
156
+ signature, # X-Printavo-Signature header value
157
+ payload, # raw request body string
158
+ secret # your webhook secret
159
+ )
160
+ ```
161
+
162
+ ### Rails Controller Example
163
+
164
+ ```ruby
165
+ class WebhooksController < ApplicationController
166
+ skip_before_action :verify_authenticity_token
167
+
168
+ def printavo
169
+ if Printavo::Webhooks.verify(
170
+ request.headers["X-Printavo-Signature"],
171
+ request.raw_post,
172
+ ENV["PRINTAVO_WEBHOOK_SECRET"]
173
+ )
174
+ event = JSON.parse(request.raw_post)
175
+ # process event["type"] ...
176
+ head :ok
177
+ else
178
+ head :unauthorized
179
+ end
180
+ end
181
+ end
182
+ ```
183
+
184
+ ## Error Handling
185
+
186
+ ```ruby
187
+ begin
188
+ client.orders.find("not_a_real_id")
189
+ rescue Printavo::AuthenticationError => e
190
+ # Bad email/token
191
+ rescue Printavo::RateLimitError => e
192
+ # Exceeded 10 req/5 sec — back off and retry
193
+ rescue Printavo::NotFoundError => e
194
+ # Resource doesn't exist
195
+ rescue Printavo::ApiError => e
196
+ # GraphQL error — e.message contains details, e.response has raw data
197
+ rescue Printavo::Error => e
198
+ # Catch-all for any Printavo error
199
+ end
200
+ ```
201
+
202
+ ## Versioning Roadmap
203
+
204
+ | Version | Milestone |
205
+ |---|---|
206
+ | 0.1.0 | Auth + Customers + Orders + Jobs |
207
+ | 0.2.0 | Status registry + Analytics/Reporting |
208
+ | 0.3.0 | Webhooks (Rack-compatible) |
209
+ | 0.4.0 | Expanded GraphQL DSL |
210
+ | 0.5.0 | Mutations (create/update) |
211
+ | 0.6.0 | Community burn-in / API stabilization |
212
+ | 0.7.0 | Pagination abstraction helpers |
213
+ | 0.8.0 | Retry/backoff + rate limit awareness |
214
+ | 0.9.0 | Community feedback + API freeze |
215
+ | 1.0.0 | Stable public SDK |
216
+
217
+ **Rules**: `PATCH` = bug fix · `MINOR` = new backward-compatible feature · `MAJOR` = breaking change
218
+
219
+ ## API Documentation
220
+
221
+ - [Printavo v2 API Reference](https://www.printavo.com/docs/api/v2)
222
+ - [Printavo GraphQL API Blog Post](https://www.printavo.com/blog/new-graphql-api/)
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ git clone https://github.com/scarver2/printavo-ruby.git
228
+ cd printavo-ruby
229
+ bundle install
230
+
231
+ # Run specs
232
+ bundle exec rspec
233
+
234
+ # Lint
235
+ bundle exec rubocop
236
+
237
+ # Guard DX (watches files, re-runs tests + lint on save)
238
+ bundle exec guard
239
+
240
+ # Interactive console
241
+ PRINTAVO_EMAIL=you@example.com PRINTAVO_TOKEN=your_token bin/console
242
+ ```
243
+
244
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for full contribution guidelines.
245
+
246
+ ## Colophon
247
+
248
+ [MIT License](LICENSE)
249
+
250
+ &copy;2026 [Stan Carver II](https://stancarver.com)
251
+
252
+ ![Made in Texas](https://raw.githubusercontent.com/scarver2/howdy-world/master/_dashboard/www/assets/made-in-texas.png)
@@ -0,0 +1,38 @@
1
+ # lib/printavo/client.rb
2
+ module Printavo
3
+ class Client
4
+ attr_reader :graphql
5
+
6
+ # Creates a new Printavo API client. Each instance is independent —
7
+ # multiple clients with different credentials can coexist in one process.
8
+ #
9
+ # @param email [String] the email address associated with your Printavo account
10
+ # @param token [String] the API token from your Printavo My Account page
11
+ # @param timeout [Integer] HTTP timeout in seconds (default: 30)
12
+ #
13
+ # @example
14
+ # client = Printavo::Client.new(
15
+ # email: ENV["PRINTAVO_EMAIL"],
16
+ # token: ENV["PRINTAVO_TOKEN"]
17
+ # )
18
+ # client.customers.all
19
+ # client.orders.find("12345")
20
+ # client.graphql.query("{ customers { nodes { id } } }")
21
+ def initialize(email:, token:, timeout: 30)
22
+ connection = Connection.new(email: email, token: token, timeout: timeout).build
23
+ @graphql = GraphqlClient.new(connection)
24
+ end
25
+
26
+ def customers
27
+ Resources::Customers.new(@graphql)
28
+ end
29
+
30
+ def orders
31
+ Resources::Orders.new(@graphql)
32
+ end
33
+
34
+ def jobs
35
+ Resources::Jobs.new(@graphql)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # lib/printavo/config.rb
2
+ module Printavo
3
+ class Config
4
+ attr_accessor :email, :token, :base_url, :timeout
5
+
6
+ BASE_URL = 'https://www.printavo.com/api/v2'.freeze
7
+
8
+ def initialize
9
+ @base_url = BASE_URL
10
+ @timeout = 30
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ # lib/printavo/connection.rb
2
+ require 'faraday'
3
+ require 'faraday/retry'
4
+
5
+ module Printavo
6
+ class Connection
7
+ def initialize(email:, token:, base_url: Config::BASE_URL, timeout: 30)
8
+ @email = email
9
+ @token = token
10
+ @base_url = base_url
11
+ @timeout = timeout
12
+ end
13
+
14
+ def build
15
+ Faraday.new(url: @base_url) do |f|
16
+ f.headers['Content-Type'] = 'application/json'
17
+ f.headers['Accept'] = 'application/json'
18
+ f.headers['email'] = @email
19
+ f.headers['token'] = @token
20
+
21
+ f.request :retry, max: 2, interval: 0.5, retry_statuses: [429, 500, 502, 503]
22
+
23
+ f.response :json
24
+ f.options.timeout = @timeout
25
+ f.options.open_timeout = @timeout
26
+
27
+ f.adapter Faraday.default_adapter
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ # lib/printavo/errors.rb
2
+ module Printavo
3
+ class Error < StandardError; end
4
+
5
+ class AuthenticationError < Error; end
6
+
7
+ class RateLimitError < Error; end
8
+
9
+ class NotFoundError < Error; end
10
+
11
+ class ApiError < Error
12
+ attr_reader :response
13
+
14
+ def initialize(message, response: nil)
15
+ super(message)
16
+ @response = response
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ # lib/printavo/graphql_client.rb
2
+ require 'json'
3
+
4
+ module Printavo
5
+ class GraphqlClient
6
+ def initialize(connection)
7
+ @connection = connection
8
+ end
9
+
10
+ def query(query_string, variables: {})
11
+ response = @connection.post('') do |req|
12
+ req.body = JSON.generate(query: query_string, variables: variables)
13
+ end
14
+
15
+ handle_response(response)
16
+ end
17
+
18
+ private
19
+
20
+ def handle_response(response)
21
+ body = response.body
22
+
23
+ case response.status
24
+ when 401 then raise AuthenticationError, 'Invalid credentials — check your email and token'
25
+ when 429 then raise RateLimitError, 'Printavo rate limit exceeded (10 req/5 sec)'
26
+ when 404 then raise NotFoundError, 'Resource not found'
27
+ end
28
+
29
+ errors = body.is_a?(Hash) ? body['errors'] : nil
30
+ if errors&.any?
31
+ messages = errors.map { |e| e['message'] }.join(', ')
32
+ raise ApiError.new(messages, response: body)
33
+ end
34
+
35
+ body.is_a?(Hash) ? body['data'] : body
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ # lib/printavo/models/base.rb
2
+ module Printavo
3
+ module Models
4
+ class Base
5
+ def initialize(attributes = {})
6
+ @attributes = (attributes || {}).transform_keys(&:to_s)
7
+ end
8
+
9
+ def [](key)
10
+ @attributes[key.to_s]
11
+ end
12
+
13
+ def dig(*keys)
14
+ keys.map(&:to_s).reduce(@attributes) do |obj, key|
15
+ break nil unless obj.is_a?(Hash)
16
+
17
+ obj[key]
18
+ end
19
+ end
20
+
21
+ def to_h
22
+ @attributes.dup
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(self.class) && other.to_h == to_h
27
+ end
28
+
29
+ def inspect
30
+ "#<#{self.class.name} #{@attributes.inspect}>"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # lib/printavo/models/customer.rb
2
+ module Printavo
3
+ class Customer < Models::Base
4
+ def id = self['id']
5
+ def first_name = self['firstName']
6
+ def last_name = self['lastName']
7
+ def email = self['email']
8
+ def phone = self['phone']
9
+ def company = self['company']
10
+
11
+ def full_name
12
+ [first_name, last_name].compact.join(' ').strip
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # lib/printavo/models/job.rb
2
+ module Printavo
3
+ class Job < Models::Base
4
+ def id = self['id']
5
+ def name = self['name']
6
+ def quantity = self['quantity']
7
+ def price = self['price']
8
+ def taxable = self['taxable']
9
+
10
+ def taxable?
11
+ taxable == true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # lib/printavo/models/order.rb
2
+ module Printavo
3
+ class Order < Models::Base
4
+ def id = self['id']
5
+ def nickname = self['nickname']
6
+ def total_price = self['totalPrice']
7
+
8
+ def status
9
+ dig('status', 'name')
10
+ end
11
+
12
+ def status_key
13
+ return nil if status.nil?
14
+
15
+ status.downcase.gsub(/\s+/, '_').to_sym
16
+ end
17
+
18
+ def status?(key)
19
+ status_key == key.to_sym
20
+ end
21
+
22
+ def customer
23
+ attrs = self['customer']
24
+ Customer.new(attrs) if attrs
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ # lib/printavo/resources/base.rb
2
+ module Printavo
3
+ module Resources
4
+ class Base
5
+ def initialize(graphql)
6
+ @graphql = graphql
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,48 @@
1
+ # lib/printavo/resources/customers.rb
2
+ module Printavo
3
+ module Resources
4
+ 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
36
+
37
+ def all(first: 25, after: nil)
38
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
39
+ data['customers']['nodes'].map { |attrs| Printavo::Customer.new(attrs) }
40
+ end
41
+
42
+ def find(id)
43
+ data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
44
+ Printavo::Customer.new(data['customer'])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # lib/printavo/resources/jobs.rb
2
+ module Printavo
3
+ module Resources
4
+ 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
36
+
37
+ def all(order_id:, first: 25, after: nil)
38
+ data = @graphql.query(ALL_QUERY, variables: { orderId: order_id.to_s, first: first, after: after })
39
+ data['order']['lineItems']['nodes'].map { |attrs| Printavo::Job.new(attrs) }
40
+ end
41
+
42
+ def find(id)
43
+ data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
44
+ Printavo::Job.new(data['lineItem'])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # lib/printavo/resources/orders.rb
2
+ module Printavo
3
+ module Resources
4
+ 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
+ }
16
+ customer {
17
+ id
18
+ firstName
19
+ lastName
20
+ email
21
+ company
22
+ }
23
+ }
24
+ pageInfo {
25
+ hasNextPage
26
+ endCursor
27
+ }
28
+ }
29
+ }
30
+ GQL
31
+
32
+ FIND_QUERY = <<~GQL.freeze
33
+ query Order($id: ID!) {
34
+ order(id: $id) {
35
+ id
36
+ nickname
37
+ totalPrice
38
+ status {
39
+ id
40
+ name
41
+ }
42
+ customer {
43
+ id
44
+ firstName
45
+ lastName
46
+ email
47
+ company
48
+ }
49
+ }
50
+ }
51
+ GQL
52
+
53
+ def all(first: 25, after: nil)
54
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
55
+ data['orders']['nodes'].map { |attrs| Printavo::Order.new(attrs) }
56
+ end
57
+
58
+ def find(id)
59
+ data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
60
+ Printavo::Order.new(data['order'])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ # lib/printavo/version.rb
2
+ module Printavo
3
+ VERSION = '0.1.0'.freeze
4
+ end
@@ -0,0 +1,34 @@
1
+ # lib/printavo/webhooks.rb
2
+ require 'openssl'
3
+
4
+ module Printavo
5
+ module Webhooks
6
+ # Verifies a Printavo webhook signature using HMAC-SHA256.
7
+ #
8
+ # Uses a constant-time comparison to prevent timing attacks.
9
+ #
10
+ # @param signature [String] the signature from the X-Printavo-Signature header
11
+ # @param payload [String] the raw request body string
12
+ # @param secret [String] your webhook secret configured in Printavo
13
+ # @return [Boolean]
14
+ #
15
+ # @example Rails controller
16
+ # if Printavo::Webhooks.verify(
17
+ # request.headers["X-Printavo-Signature"],
18
+ # request.raw_post,
19
+ # ENV["PRINTAVO_WEBHOOK_SECRET"]
20
+ # )
21
+ # # process event
22
+ # else
23
+ # head :unauthorized
24
+ # end
25
+ def self.verify(signature, payload, secret)
26
+ return false if signature.nil? || payload.nil? || secret.nil?
27
+
28
+ expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
29
+ OpenSSL.secure_compare(expected, signature)
30
+ rescue ArgumentError, TypeError
31
+ false
32
+ end
33
+ end
34
+ end
data/lib/printavo.rb ADDED
@@ -0,0 +1,23 @@
1
+ # lib/printavo.rb
2
+ require 'faraday'
3
+ require 'faraday/retry'
4
+ require 'json'
5
+
6
+ require_relative 'printavo/version'
7
+ require_relative 'printavo/errors'
8
+ require_relative 'printavo/config'
9
+ require_relative 'printavo/connection'
10
+ require_relative 'printavo/graphql_client'
11
+ require_relative 'printavo/models/base'
12
+ require_relative 'printavo/models/customer'
13
+ require_relative 'printavo/models/order'
14
+ require_relative 'printavo/models/job'
15
+ require_relative 'printavo/resources/base'
16
+ require_relative 'printavo/resources/customers'
17
+ require_relative 'printavo/resources/orders'
18
+ require_relative 'printavo/resources/jobs'
19
+ require_relative 'printavo/webhooks'
20
+ require_relative 'printavo/client'
21
+
22
+ module Printavo
23
+ end
metadata ADDED
@@ -0,0 +1,309 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: printavo-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stan Carver II
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '13.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '13.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec_junit_formatter
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.6'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-performance
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-rake
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.6'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.6'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop-rspec
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '3.0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '3.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: simplecov
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '0.22'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '0.22'
209
+ - !ruby/object:Gem::Dependency
210
+ name: simplecov-lcov
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '0.8'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '0.8'
223
+ - !ruby/object:Gem::Dependency
224
+ name: vcr
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '6.0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: '6.0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: webmock
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '3.0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '3.0'
251
+ description: |
252
+ A framework-agnostic Ruby SDK for Printavo supporting the v2 GraphQL API.
253
+ Provides both a resource-oriented interface (client.orders.all) and rich domain
254
+ models (Printavo::Order), plus raw GraphQL access and Rack-compatible webhook
255
+ verification. Built to bridge Printavo with external operational systems.
256
+ email:
257
+ - stan@stancarver.com
258
+ executables: []
259
+ extensions: []
260
+ extra_rdoc_files: []
261
+ files:
262
+ - CHANGELOG.md
263
+ - LICENSE
264
+ - README.md
265
+ - lib/printavo.rb
266
+ - lib/printavo/client.rb
267
+ - lib/printavo/config.rb
268
+ - lib/printavo/connection.rb
269
+ - lib/printavo/errors.rb
270
+ - lib/printavo/graphql_client.rb
271
+ - lib/printavo/models/base.rb
272
+ - lib/printavo/models/customer.rb
273
+ - lib/printavo/models/job.rb
274
+ - lib/printavo/models/order.rb
275
+ - lib/printavo/resources/base.rb
276
+ - lib/printavo/resources/customers.rb
277
+ - lib/printavo/resources/jobs.rb
278
+ - lib/printavo/resources/orders.rb
279
+ - lib/printavo/version.rb
280
+ - lib/printavo/webhooks.rb
281
+ homepage: https://github.com/scarver2/printavo-ruby
282
+ licenses:
283
+ - MIT
284
+ metadata:
285
+ bug_tracker_uri: https://github.com/scarver2/printavo-ruby/issues
286
+ changelog_uri: https://github.com/scarver2/printavo-ruby/blob/master/CHANGELOG.md
287
+ documentation_uri: https://github.com/scarver2/printavo-ruby#readme
288
+ source_code_uri: https://github.com/scarver2/printavo-ruby
289
+ rubygems_mfa_required: 'true'
290
+ post_install_message:
291
+ rdoc_options: []
292
+ require_paths:
293
+ - lib
294
+ required_ruby_version: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '3.0'
299
+ required_rubygems_version: !ruby/object:Gem::Requirement
300
+ requirements:
301
+ - - ">="
302
+ - !ruby/object:Gem::Version
303
+ version: '0'
304
+ requirements: []
305
+ rubygems_version: 3.5.22
306
+ signing_key:
307
+ specification_version: 4
308
+ summary: Ruby SDK for the Printavo GraphQL API
309
+ test_files: []