printavo-ruby 0.1.0 → 0.3.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: f47615b00a6d63bc35e5d5cc74e5e8d591fe38218eadd0bb76e11798626f9f42
4
- data.tar.gz: aa6d86cb12b1ea7617c8d97093993252fc30f1401fef8893263ed2ba4f277a48
3
+ metadata.gz: a7786dadca364f38068636a9a6e30efac78ac79107c8dfb4ade22b39db5f0bc1
4
+ data.tar.gz: f0464978791f12c8e3f5d37d2de09ecfc4b97e57aca4999beda995b68957d2e1
5
5
  SHA512:
6
- metadata.gz: '08fa3eb45c8ddef4ff5962c9bd59d01c14048d2c68fefb1b3e6ee526b6e4317b225958b5ed8865ba45fb42b35ef14d1c8f235a81b90a3c11e1e4dd661e2c1aa4'
7
- data.tar.gz: 93ef9341891c36ab6ffa9fd61a9790b16662072a7ecb4bf733d6aa0f5cd9fe99ee75c77a47be7a023909b7468fb70abec3a8d5b076925a732c3cd8c6389ec1b6
6
+ metadata.gz: a6aae8c15d5f5c348d8e717c09eaf85092a9421d473541ac1ef3c38042534ed1aa14c6e0c5283d6878325cdf32a7325fed52e980913e7b15e8c443ed64a695bb
7
+ data.tar.gz: bac95ec4243aa408c495f88d7c4841a308f7c49f509d8b7a0f3a4a161cb9db30418d2ba2c79be67f353c89b68d24cec6f70c1487b64496ea5ef74489fe44acc8
data/README.md CHANGED
@@ -116,6 +116,57 @@ job = client.jobs.find("77")
116
116
  puts job.taxable? # => true
117
117
  ```
118
118
 
119
+ ### Pagination
120
+
121
+ All list resources support `each_page` and `all_pages` in addition to `all`.
122
+
123
+ ```ruby
124
+ # Iterate page by page (cursor-based, memory-efficient)
125
+ client.customers.each_page(first: 50) do |records|
126
+ records.each { |c| puts c.full_name }
127
+ end
128
+
129
+ # Collect every record across all pages into one array
130
+ all_orders = client.orders.all_pages
131
+
132
+ # Jobs require order_id
133
+ client.jobs.each_page(order_id: "99") do |records|
134
+ records.each { |j| puts j.name }
135
+ end
136
+ all_jobs = client.jobs.all_pages(order_id: "99")
137
+ ```
138
+
139
+ ### Statuses
140
+
141
+ ```ruby
142
+ # List all statuses
143
+ statuses = client.statuses.all
144
+ statuses.each { |s| puts "#{s.name} (#{s.color})" }
145
+
146
+ # Build a registry for O(1) lookup by symbol key
147
+ registry = client.statuses.registry
148
+ registry[:in_production] # => <Printavo::Status>
149
+ registry[:in_production].color # => "#ff6600"
150
+
151
+ # Pair with an order's status_key
152
+ order = client.orders.find("99")
153
+ status = registry[order.status_key]
154
+ puts "#{order.status} — #{status.color}"
155
+ ```
156
+
157
+ ### Inquiries
158
+
159
+ ```ruby
160
+ # List inquiries (quotes / leads)
161
+ inquiries = client.inquiries.all
162
+ inquiries.each { |i| puts "#{i.nickname}: #{i.status}" }
163
+
164
+ # Find a specific inquiry
165
+ inquiry = client.inquiries.find("55")
166
+ puts inquiry.status?(:new_inquiry) # => true
167
+ puts inquiry.customer.full_name # => "Jane Smith"
168
+ ```
169
+
119
170
  ### Raw GraphQL
120
171
 
121
172
  For queries not yet wrapped by a resource, use the raw GraphQL client directly:
@@ -203,15 +254,15 @@ end
203
254
 
204
255
  | Version | Milestone |
205
256
  |---|---|
206
- | 0.1.0 | Auth + Customers + Orders + Jobs |
207
- | 0.2.0 | Status registry + Analytics/Reporting |
208
- | 0.3.0 | Webhooks (Rack-compatible) |
257
+ | 0.1.0 | Auth + Customers + Orders + Jobs + Webhooks (Rack-compatible) ✅ |
258
+ | 0.2.0 | Status registry + Inquiries |
259
+ | 0.3.0 | Pagination helpers (`each_page`, `all_pages`) + `bin/lint` ✅ |
209
260
  | 0.4.0 | Expanded GraphQL DSL |
210
261
  | 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 |
262
+ | 0.6.0 | Analytics / Reporting queries |
263
+ | 0.7.0 | Community burn-in / API stabilization |
264
+ | 0.8.0 | Pagination abstraction helpers |
265
+ | 0.9.0 | Retry/backoff + rate limit awareness |
215
266
  | 1.0.0 | Stable public SDK |
216
267
 
217
268
  **Rules**: `PATCH` = bug fix · `MINOR` = new backward-compatible feature · `MAJOR` = breaking change
@@ -241,7 +292,7 @@ bundle exec guard
241
292
  PRINTAVO_EMAIL=you@example.com PRINTAVO_TOKEN=your_token bin/console
242
293
  ```
243
294
 
244
- See [CONTRIBUTING.md](CONTRIBUTING.md) for full contribution guidelines.
295
+ See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for full contribution guidelines.
245
296
 
246
297
  ## Colophon
247
298
 
data/docs/CACHING.md ADDED
@@ -0,0 +1,233 @@
1
+ <!-- docs/CACHING.md -->
2
+ # Caching Strategies for printavo-ruby
3
+
4
+ The Printavo API enforces a **rate limit of 10 requests per 5 seconds** per account.
5
+ For applications that read Printavo data frequently (dashboards, reporting tools,
6
+ CRM syncs), caching is essential to stay within this limit and reduce latency.
7
+
8
+ This guide covers practical caching patterns, from zero-dependency Ruby to
9
+ Redis and Rails.cache, along with recommended TTLs for each resource type.
10
+
11
+ ---
12
+
13
+ ## Why Cache Printavo Responses?
14
+
15
+ | Factor | Detail |
16
+ |---|---|
17
+ | Rate limit | 10 req / 5 sec per account |
18
+ | Data volatility | Customers and statuses rarely change; orders change moderately |
19
+ | Use case | Dashboards, CRM syncs, and reporting hammer read endpoints repeatedly |
20
+ | Cost | Fewer API calls = faster responses + less risk of throttling |
21
+
22
+ ---
23
+
24
+ ## Strategy 1: In-Memory Caching (Zero Dependencies)
25
+
26
+ Best for: **scripts, background jobs, single-request data batches**
27
+
28
+ A plain Ruby hash is enough when you only need to deduplicate calls
29
+ within a single process run:
30
+
31
+ ```ruby
32
+ customer_cache = {}
33
+
34
+ def fetch_customer(client, id, cache)
35
+ cache[id] ||= client.customers.find(id)
36
+ end
37
+
38
+ order = client.orders.find("99")
39
+ customer = fetch_customer(client, order.customer.id, customer_cache)
40
+ ```
41
+
42
+ > Note: In-memory caches are cleared on process restart and are not shared
43
+ > across processes or threads (without synchronization).
44
+
45
+ ---
46
+
47
+ ## Strategy 2: Status Registry Caching
48
+
49
+ Best for: **status-heavy workflows**
50
+
51
+ Printavo statuses are user-defined and rarely change. Cache them once at
52
+ startup to avoid repeated API calls when checking `order.status?`:
53
+
54
+ ```ruby
55
+ # Fetch all statuses once and freeze them
56
+ raw = client.graphql.query(<<~GQL)
57
+ {
58
+ statuses {
59
+ nodes { id name }
60
+ }
61
+ }
62
+ GQL
63
+
64
+ STATUS_REGISTRY = raw["statuses"]["nodes"].freeze
65
+ # => [{"id"=>"1", "name"=>"Quote"}, {"id"=>"2", "name"=>"In Production"}, ...]
66
+ ```
67
+
68
+ This pairs well with `Printavo::Order#status_key` for symbol-based lookups.
69
+
70
+ ---
71
+
72
+ ## Strategy 3: Rails.cache Integration
73
+
74
+ Best for: **Rails applications** (recommended default)
75
+
76
+ Wrap any Printavo call in `Rails.cache.fetch` to use whatever cache store
77
+ your app already has configured (Redis, Memcache, memory, etc.):
78
+
79
+ ```ruby
80
+ # config/initializers/printavo.rb
81
+ PRINTAVO = Printavo::Client.new(
82
+ email: ENV["PRINTAVO_EMAIL"],
83
+ token: ENV["PRINTAVO_TOKEN"]
84
+ )
85
+ ```
86
+
87
+ ```ruby
88
+ # In a service, controller, or job:
89
+ def customers
90
+ Rails.cache.fetch("printavo:customers", expires_in: 10.minutes) do
91
+ PRINTAVO.customers.all
92
+ end
93
+ end
94
+
95
+ def order(id)
96
+ Rails.cache.fetch("printavo:order:#{id}", expires_in: 2.minutes) do
97
+ PRINTAVO.orders.find(id)
98
+ end
99
+ end
100
+ ```
101
+
102
+ > Use namespaced cache keys (`printavo:resource:id`) to make invalidation
103
+ > easier and to avoid collisions with other app cache entries.
104
+
105
+ ---
106
+
107
+ ## Strategy 4: Redis (Framework-Agnostic)
108
+
109
+ Best for: **non-Rails Ruby applications** that need a shared, persistent cache
110
+
111
+ ```ruby
112
+ require "redis"
113
+ require "json"
114
+
115
+ REDIS = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379"))
116
+ PRINTAVO_CLIENT = Printavo::Client.new(
117
+ email: ENV["PRINTAVO_EMAIL"],
118
+ token: ENV["PRINTAVO_TOKEN"]
119
+ )
120
+
121
+ def cached_customer(id, ttl: 600)
122
+ key = "printavo:customer:#{id}"
123
+ data = REDIS.get(key)
124
+
125
+ if data
126
+ Printavo::Customer.new(JSON.parse(data))
127
+ else
128
+ customer = PRINTAVO_CLIENT.customers.find(id)
129
+ REDIS.setex(key, ttl, JSON.generate(customer.to_h))
130
+ customer
131
+ end
132
+ end
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Strategy 5: Webhook-Driven Cache Invalidation
138
+
139
+ Best for: **keeping cache fresh without polling**
140
+
141
+ When Printavo sends a webhook event (order updated, customer changed),
142
+ invalidate the specific cache entry immediately:
143
+
144
+ ```ruby
145
+ # Rails controller
146
+ class WebhooksController < ApplicationController
147
+ skip_before_action :verify_authenticity_token
148
+
149
+ def printavo
150
+ unless Printavo::Webhooks.verify(
151
+ request.headers["X-Printavo-Signature"],
152
+ request.raw_post,
153
+ ENV["PRINTAVO_WEBHOOK_SECRET"]
154
+ )
155
+ return head :unauthorized
156
+ end
157
+
158
+ event = JSON.parse(request.raw_post)
159
+ invalidate_cache(event)
160
+ head :ok
161
+ end
162
+
163
+ private
164
+
165
+ def invalidate_cache(event)
166
+ case event["type"]
167
+ when "order.updated", "order.created"
168
+ Rails.cache.delete("printavo:order:#{event.dig("data", "id")}")
169
+ when "customer.updated"
170
+ Rails.cache.delete("printavo:customer:#{event.dig("data", "id")}")
171
+ Rails.cache.delete("printavo:customers")
172
+ end
173
+ end
174
+ end
175
+ ```
176
+
177
+ This pattern gives you **real-time accuracy** with **minimal API calls**.
178
+
179
+ ---
180
+
181
+ ## Recommended TTLs
182
+
183
+ | Resource | Suggested TTL | Rationale |
184
+ |---|---|---|
185
+ | Customers (list) | 10 minutes | Changes infrequently |
186
+ | Customer (single) | 10 minutes | Same |
187
+ | Orders (list) | 2 minutes | Status changes often during production |
188
+ | Order (single) | 2 minutes | Same |
189
+ | Statuses/enums | 1 hour | Rarely changed by shop admins |
190
+ | Analytics/reports | 15 minutes | Acceptable staleness for reporting |
191
+
192
+ Adjust these TTLs based on your shop's actual order velocity and
193
+ how frequently staff update records.
194
+
195
+ ---
196
+
197
+ ## What NOT to Cache
198
+
199
+ | Item | Reason |
200
+ |---|---|
201
+ | API credentials | Managed by Printavo; don't duplicate |
202
+ | Full paginated collections | Cache individual pages by cursor key instead |
203
+ | Results immediately after a mutation | Always re-fetch after writes |
204
+ | Webhook payloads | Process once and discard |
205
+
206
+ ---
207
+
208
+ ## Future: Built-In Cache Adapter
209
+
210
+ A future version of `printavo-ruby` may support an optional cache adapter
211
+ passed directly to the client:
212
+
213
+ ```ruby
214
+ # Possible future API — not implemented in v0.x
215
+ client = Printavo::Client.new(
216
+ email: ENV["PRINTAVO_EMAIL"],
217
+ token: ENV["PRINTAVO_TOKEN"],
218
+ cache: Rails.cache, # any object responding to fetch/delete
219
+ default_ttl: 300
220
+ )
221
+
222
+ # Would cache automatically:
223
+ client.customers.all # cached for 300s by default
224
+ client.orders.find(1) # cached per-id
225
+ ```
226
+
227
+ Track this feature in [FUTURE.md](../FUTURE.md).
228
+
229
+ ---
230
+
231
+ Stan Carver II
232
+ Made in Texas 🤠
233
+ https://stancarver.com
data/docs/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ <!-- docs/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.3.0] - 2026-03-29
12
+
13
+ ### Added
14
+ - `Printavo::Page` — value object wrapping `records`, `has_next_page`, `end_cursor` with `to_a`, `size`, `empty?`
15
+ - `Printavo::Resources::Base#each_page` — yields each page of records as an Array, following cursors automatically
16
+ - `Printavo::Resources::Base#all_pages` — returns all records across all pages as a flat Array
17
+ - All resources (`Customers`, `Orders`, `Jobs`, `Statuses`, `Inquiries`) implement `fetch_page` and support `each_page`/`all_pages`
18
+ - `Jobs#each_page(order_id:)` and `Jobs#all_pages(order_id:)` — `order_id` forwarded via `**kwargs`
19
+ - `bin/lint` — multi-Ruby RuboCop runner mirroring `bin/spec` (reads versions from `.mise.toml`)
20
+ - Roadmap: moved Pagination from 0.8.0 to 0.3.0; Webhooks slot repurposed
21
+
22
+ ### Changed
23
+ - All resource `all` methods refactored to delegate to `fetch_page` (backward compatible — still returns `Array`)
24
+
25
+ ## [0.2.0] - 2026-03-29
26
+
27
+ ### Added
28
+ - `Printavo::Status` — domain model with `id`, `name`, `color`, `key` (snake\_case symbol)
29
+ - `Printavo::Resources::Statuses` — `all`, `find`, and `registry` (O(1) Hash lookup by symbol key)
30
+ - `Printavo::Inquiry` — domain model mirroring `Order` with status predicate helpers
31
+ - `Printavo::Resources::Inquiries` — `all` and `find` for Printavo quote/inquiry records
32
+ - `Printavo::Order#status_id` and `#status_color` — expose full status sub-object fields
33
+ - `client.statuses` and `client.inquiries` — new entry points on `Printavo::Client`
34
+ - Orders GraphQL query now fetches `status { id name color }` for full status data
35
+ - Moved `CHANGELOG.md`, `CONTRIBUTING.md`, `FUTURE.md` to `docs/` for cleaner repo root
36
+
37
+ ### Changed
38
+ - Gemspec `changelog_uri` updated to `docs/CHANGELOG.md`
39
+ - Gemspec `spec.files` updated to include `docs/**/*.md`
40
+
41
+ ## [0.1.0] - 2026-03-29
42
+
43
+ ### Added
44
+ - `Printavo::Client` — instance-based, multi-client capable entry point
45
+ - `Printavo::Resources::Customers` — `all` and `find` via GraphQL
46
+ - `Printavo::Resources::Orders` — `all` and `find` with status + customer association
47
+ - `Printavo::Resources::Jobs` — `all` (by order) and `find` line item queries
48
+ - `Printavo::Customer`, `Printavo::Order`, `Printavo::Job` — rich domain models
49
+ - `Printavo::Order#status?` — dynamic status predicate (handles user-defined statuses)
50
+ - `Printavo::GraphqlClient` — raw GraphQL query/mutation interface
51
+ - `Printavo::Webhooks.verify` — Rack-compatible HMAC-SHA256 signature verification
52
+ - Error hierarchy: `AuthenticationError`, `RateLimitError`, `NotFoundError`, `ApiError`
53
+ - Faraday connection with retry middleware (max 2 retries; 429/5xx)
54
+ - RSpec test suite — 62 examples, 100% line coverage with VCR + WebMock + Faker sanitization
55
+ - Coveralls coverage badge (LCOV via `simplecov-lcov`)
56
+ - Guard + RuboCop DX setup with `bin/spec` multi-Ruby local runner
57
+ - GitHub Actions CI: Ruby 3.3 (primary) + Ruby 4.0 (`continue-on-error`)
58
+ - Automated RubyGems publish on `v*` tag via `release.yml`
59
+ - `docs/CACHING.md` — nine caching strategy patterns for rate-limit-aware consumers
60
+
61
+ ---
62
+
63
+ — Stan Carver II / Made in Texas 🤠 / https://stancarver.com
@@ -0,0 +1,87 @@
1
+ <!-- docs/CONTRIBUTING.md -->
2
+ # Contributing to printavo-ruby
3
+
4
+ Thank you for your interest in contributing! This guide covers setup, workflow,
5
+ and standards for working on `printavo-ruby`.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ git clone https://github.com/scarver2/printavo-ruby.git
11
+ cd printavo-ruby
12
+ bundle install
13
+ ```
14
+
15
+ ## Running Tests
16
+
17
+ ```bash
18
+ bundle exec rspec
19
+ ```
20
+
21
+ Coverage is tracked via SimpleCov. The minimum threshold is **90%**.
22
+ New code must include specs.
23
+
24
+ ## Guard DX (Recommended)
25
+
26
+ Guard watches for file changes and re-runs specs and RuboCop automatically:
27
+
28
+ ```bash
29
+ bundle exec guard
30
+ ```
31
+
32
+ ## Coding Standards
33
+
34
+ This project uses [RuboCop](https://rubocop.org/) with the
35
+ `rubocop-performance`, `rubocop-rake`, and `rubocop-rspec` extensions.
36
+
37
+ ```bash
38
+ bundle exec rubocop
39
+ bundle exec rubocop -a # auto-correct safe offenses
40
+ ```
41
+
42
+ ## VCR Cassettes
43
+
44
+ Integration tests use [VCR](https://github.com/vcr/vcr) to record and replay
45
+ HTTP interactions. All cassettes **must be sanitized** before committing:
46
+
47
+ - Real email addresses → `customer@example.com`
48
+ - Real phone numbers → `555-867-5309`
49
+ - Real customer names → `Acme Customer`
50
+ - API credentials are filtered automatically by `spec/support/vcr.rb`
51
+
52
+ Use the A1Web demo Printavo account when recording new cassettes — never
53
+ record against production TER data.
54
+
55
+ ## Pull Request Guidelines
56
+
57
+ - Branch from `master`
58
+ - One feature / bug fix per PR
59
+ - All CI checks must pass (RSpec + RuboCop across Ruby 3.1–4.0)
60
+ - Write specs for any new code
61
+ - Update `docs/CHANGELOG.md` with a summary of changes
62
+
63
+ ## Version Bump Rules
64
+
65
+ Versions follow [Semantic Versioning](https://semver.org/):
66
+
67
+ | Change type | Version part |
68
+ |---|---|
69
+ | Bug fix | PATCH (0.1.x) |
70
+ | New backward-compatible feature | MINOR (0.x.0) |
71
+ | Breaking API change | MAJOR (x.0.0) |
72
+
73
+ Bump `lib/printavo/version.rb`, update `docs/CHANGELOG.md`, then:
74
+
75
+ ```bash
76
+ git commit -am "Release vX.Y.Z"
77
+ git tag vX.Y.Z
78
+ git push origin master --tags
79
+ ```
80
+
81
+ GitHub Actions will build and push to RubyGems automatically on tag push.
82
+
83
+ ---
84
+
85
+ Stan Carver II
86
+ Made in Texas 🤠
87
+ https://stancarver.com
data/docs/FUTURE.md ADDED
@@ -0,0 +1,116 @@
1
+ <!-- docs/FUTURE.md -->
2
+ # Future Roadmap
3
+
4
+ Ideas and planned features for `printavo-ruby` that are out of scope for the
5
+ initial `0.x` releases. Contributions and discussions welcome!
6
+
7
+ ## Planned Features
8
+
9
+ ### CLI (Thor-based)
10
+
11
+ A `printavo` command-line tool built with [Thor](https://github.com/rails/thor):
12
+
13
+ ```bash
14
+ printavo customers
15
+ printavo orders
16
+ printavo orders find 12345
17
+ printavo analytics revenue
18
+ printavo sync orders --to crm
19
+ ```
20
+
21
+ Planned version: `0.7.0`
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
+ ### Retry/Backoff
39
+
40
+ Intelligent rate limit handling with exponential backoff:
41
+
42
+ ```ruby
43
+ client = Printavo::Client.new(
44
+ email: ENV["PRINTAVO_EMAIL"],
45
+ token: ENV["PRINTAVO_TOKEN"],
46
+ max_retries: 3,
47
+ retry_on_rate_limit: true
48
+ )
49
+ ```
50
+
51
+ Planned version: `0.9.0`
52
+
53
+ ### Analytics / Reporting Expansion
54
+
55
+ Richer wrappers for Printavo's analytics queries (revenue, job counts,
56
+ customer activity, turnaround times).
57
+
58
+ Planned version: `0.6.0`
59
+
60
+ ### Mutations (Create / Update)
61
+
62
+ Support for creating and updating resources:
63
+
64
+ ```ruby
65
+ client.customers.create(first_name: "Jane", last_name: "Smith", email: "jane@example.com")
66
+ client.orders.update("99", nickname: "Rush Job")
67
+ ```
68
+
69
+ Planned version: `0.5.0`
70
+
71
+ ### Built-In Cache Adapter
72
+
73
+ Optional cache layer that plugs into any cache store:
74
+
75
+ ```ruby
76
+ client = Printavo::Client.new(
77
+ email: ENV["PRINTAVO_EMAIL"],
78
+ token: ENV["PRINTAVO_TOKEN"],
79
+ cache: Rails.cache # or a Redis client, etc.
80
+ )
81
+ ```
82
+
83
+ See [docs/CACHING.md](docs/CACHING.md) for current caching recommendations.
84
+
85
+ ## Visualization
86
+
87
+ ### Workflow Diagram Generation (SVG/PNG)
88
+
89
+ Generate a visual map of a shop's Printavo status workflow:
90
+
91
+ ```ruby
92
+ client.workflow.diagram(format: :svg)
93
+ # => Outputs an SVG flowchart: Quote → Approved → In Production → Completed
94
+ ```
95
+
96
+ Implementation options:
97
+ - [ruby-graphviz](https://github.com/glejeune/Ruby-Graphviz) — DOT → SVG/PNG
98
+ - Pure Ruby → Mermaid output (copy-paste into docs or GitHub markdown)
99
+
100
+ ## Multi-Language SDK Family
101
+
102
+ `printavo-ruby` is the first gem in a planned multi-language SDK family:
103
+
104
+ | Repo | Language | Status |
105
+ |---|---|---|
106
+ | `printavo-ruby` | Ruby | Active |
107
+ | `printavo-python` | Python | Planned |
108
+ | `printavo-swift` | Swift | Planned |
109
+ | `printavo-zig` | Zig | Planned |
110
+ | `printavo-odin` | Odin | Planned |
111
+
112
+ ---
113
+
114
+ Stan Carver II
115
+ Made in Texas 🤠
116
+ https://stancarver.com
@@ -27,6 +27,10 @@ module Printavo
27
27
  Resources::Customers.new(@graphql)
28
28
  end
29
29
 
30
+ def statuses
31
+ Resources::Statuses.new(@graphql)
32
+ end
33
+
30
34
  def orders
31
35
  Resources::Orders.new(@graphql)
32
36
  end
@@ -34,5 +38,9 @@ module Printavo
34
38
  def jobs
35
39
  Resources::Jobs.new(@graphql)
36
40
  end
41
+
42
+ def inquiries
43
+ Resources::Inquiries.new(@graphql)
44
+ end
37
45
  end
38
46
  end
@@ -0,0 +1,27 @@
1
+ # lib/printavo/models/inquiry.rb
2
+ module Printavo
3
+ class Inquiry < 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
@@ -9,6 +9,14 @@ module Printavo
9
9
  dig('status', 'name')
10
10
  end
11
11
 
12
+ def status_id
13
+ dig('status', 'id')
14
+ end
15
+
16
+ def status_color
17
+ dig('status', 'color')
18
+ end
19
+
12
20
  def status_key
13
21
  return nil if status.nil?
14
22
 
@@ -0,0 +1,16 @@
1
+ # lib/printavo/models/status.rb
2
+ module Printavo
3
+ class Status < Models::Base
4
+ def id = self['id']
5
+ def name = self['name']
6
+ def color = self['color']
7
+
8
+ # Returns a normalized symbol key matching Order#status_key.
9
+ # e.g. "In Production" => :in_production
10
+ def key
11
+ return nil if name.nil?
12
+
13
+ name.downcase.gsub(/\s+/, '_').to_sym
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # lib/printavo/page.rb
2
+ module Printavo
3
+ # Wraps a single page of API results with cursor metadata.
4
+ #
5
+ # @example Iterating pages manually
6
+ # page = client.customers.fetch_page(first: 10)
7
+ # page.records.each { |c| puts c.full_name }
8
+ # puts page.has_next_page # => true
9
+ # puts page.end_cursor # => "cursor_abc123"
10
+ #
11
+ # @example Using each_page
12
+ # client.customers.each_page(first: 10) do |records|
13
+ # records.each { |c| puts c.full_name }
14
+ # end
15
+ Page = Struct.new(:records, :has_next_page, :end_cursor, keyword_init: true) do
16
+ def to_a = records
17
+ def size = records.size
18
+ def empty? = records.empty?
19
+ end
20
+ end
@@ -5,6 +5,47 @@ module Printavo
5
5
  def initialize(graphql)
6
6
  @graphql = graphql
7
7
  end
8
+
9
+ # Yields each page of records as an Array, following cursors automatically.
10
+ # Extra keyword arguments are forwarded to fetch_page (e.g. order_id: for Jobs).
11
+ #
12
+ # @param first [Integer] page size (default 25)
13
+ # @yieldparam records [Array] one page of domain model objects
14
+ #
15
+ # @example
16
+ # client.customers.each_page(first: 50) do |records|
17
+ # records.each { |c| puts c.full_name }
18
+ # end
19
+ def each_page(first: 25, **kwargs)
20
+ after = nil
21
+ loop do
22
+ page = fetch_page(first: first, after: after, **kwargs)
23
+ yield(page.records)
24
+ break unless page.has_next_page
25
+
26
+ after = page.end_cursor
27
+ end
28
+ end
29
+
30
+ # Returns all records across all pages as a flat Array.
31
+ # Extra keyword arguments are forwarded to fetch_page (e.g. order_id: for Jobs).
32
+ #
33
+ # @param first [Integer] page size per request (default 25)
34
+ # @return [Array]
35
+ #
36
+ # @example
37
+ # all_customers = client.customers.all_pages
38
+ # all_jobs = client.jobs.all_pages(order_id: "99")
39
+ def all_pages(first: 25, **kwargs)
40
+ [].tap { |all| each_page(first: first, **kwargs) { |records| all.concat(records) } }
41
+ end
42
+
43
+ private
44
+
45
+ # Subclasses must implement: returns a Printavo::Page.
46
+ def fetch_page(**)
47
+ raise NotImplementedError, "#{self.class}#fetch_page is not implemented"
48
+ end
8
49
  end
9
50
  end
10
51
  end
@@ -35,14 +35,26 @@ module Printavo
35
35
  GQL
36
36
 
37
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) }
38
+ fetch_page(first: first, after: after).records
40
39
  end
41
40
 
42
41
  def find(id)
43
42
  data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
44
43
  Printavo::Customer.new(data['customer'])
45
44
  end
45
+
46
+ private
47
+
48
+ def fetch_page(first: 25, after: nil, **)
49
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
50
+ nodes = data['customers']['nodes'].map { |attrs| Printavo::Customer.new(attrs) }
51
+ page_info = data['customers']['pageInfo']
52
+ Printavo::Page.new(
53
+ records: nodes,
54
+ has_next_page: page_info['hasNextPage'],
55
+ end_cursor: page_info['endCursor']
56
+ )
57
+ end
46
58
  end
47
59
  end
48
60
  end
@@ -0,0 +1,78 @@
1
+ # lib/printavo/resources/inquiries.rb
2
+ module Printavo
3
+ module Resources
4
+ 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
54
+
55
+ def all(first: 25, after: nil)
56
+ fetch_page(first: first, after: after).records
57
+ end
58
+
59
+ def find(id)
60
+ data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
61
+ Printavo::Inquiry.new(data['inquiry'])
62
+ end
63
+
64
+ private
65
+
66
+ def fetch_page(first: 25, after: nil, **)
67
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
68
+ nodes = data['inquiries']['nodes'].map { |attrs| Printavo::Inquiry.new(attrs) }
69
+ page_info = data['inquiries']['pageInfo']
70
+ Printavo::Page.new(
71
+ records: nodes,
72
+ has_next_page: page_info['hasNextPage'],
73
+ end_cursor: page_info['endCursor']
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -35,14 +35,29 @@ module Printavo
35
35
  GQL
36
36
 
37
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) }
38
+ fetch_page(order_id: order_id, first: first, after: after).records
40
39
  end
41
40
 
42
41
  def find(id)
43
42
  data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
44
43
  Printavo::Job.new(data['lineItem'])
45
44
  end
45
+
46
+ private
47
+
48
+ def fetch_page(order_id:, first: 25, after: nil, **)
49
+ data = @graphql.query(
50
+ ALL_QUERY,
51
+ variables: { orderId: order_id.to_s, first: first, after: after }
52
+ )
53
+ nodes = data['order']['lineItems']['nodes'].map { |attrs| Printavo::Job.new(attrs) }
54
+ page_info = data['order']['lineItems']['pageInfo']
55
+ Printavo::Page.new(
56
+ records: nodes,
57
+ has_next_page: page_info['hasNextPage'],
58
+ end_cursor: page_info['endCursor']
59
+ )
60
+ end
46
61
  end
47
62
  end
48
63
  end
@@ -12,6 +12,7 @@ module Printavo
12
12
  status {
13
13
  id
14
14
  name
15
+ color
15
16
  }
16
17
  customer {
17
18
  id
@@ -38,6 +39,7 @@ module Printavo
38
39
  status {
39
40
  id
40
41
  name
42
+ color
41
43
  }
42
44
  customer {
43
45
  id
@@ -51,14 +53,26 @@ module Printavo
51
53
  GQL
52
54
 
53
55
  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
+ fetch_page(first: first, after: after).records
56
57
  end
57
58
 
58
59
  def find(id)
59
60
  data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
60
61
  Printavo::Order.new(data['order'])
61
62
  end
63
+
64
+ private
65
+
66
+ def fetch_page(first: 25, after: nil, **)
67
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
68
+ nodes = data['orders']['nodes'].map { |attrs| Printavo::Order.new(attrs) }
69
+ page_info = data['orders']['pageInfo']
70
+ Printavo::Page.new(
71
+ records: nodes,
72
+ has_next_page: page_info['hasNextPage'],
73
+ end_cursor: page_info['endCursor']
74
+ )
75
+ end
62
76
  end
63
77
  end
64
78
  end
@@ -0,0 +1,62 @@
1
+ # lib/printavo/resources/statuses.rb
2
+ module Printavo
3
+ module Resources
4
+ 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
30
+
31
+ def all(first: 100, after: nil)
32
+ fetch_page(first: first, after: after).records
33
+ end
34
+
35
+ def find(id)
36
+ data = @graphql.query(FIND_QUERY, variables: { id: id.to_s })
37
+ Printavo::Status.new(data['status'])
38
+ end
39
+
40
+ # Returns a Hash{Symbol => Status} keyed by Status#key for O(1) lookup.
41
+ # Pairs with Order#status_key:
42
+ # registry = client.statuses.registry
43
+ # registry[order.status_key] #=> <Printavo::Status>
44
+ def registry
45
+ all.to_h { |status| [status.key, status] }
46
+ end
47
+
48
+ private
49
+
50
+ def fetch_page(first: 100, after: nil, **)
51
+ data = @graphql.query(ALL_QUERY, variables: { first: first, after: after })
52
+ nodes = data['statuses']['nodes'].map { |attrs| Printavo::Status.new(attrs) }
53
+ page_info = data['statuses']['pageInfo']
54
+ Printavo::Page.new(
55
+ records: nodes,
56
+ has_next_page: page_info['hasNextPage'],
57
+ end_cursor: page_info['endCursor']
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,4 +1,4 @@
1
1
  # lib/printavo/version.rb
2
2
  module Printavo
3
- VERSION = '0.1.0'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
data/lib/printavo.rb CHANGED
@@ -8,14 +8,19 @@ require_relative 'printavo/errors'
8
8
  require_relative 'printavo/config'
9
9
  require_relative 'printavo/connection'
10
10
  require_relative 'printavo/graphql_client'
11
+ require_relative 'printavo/page'
11
12
  require_relative 'printavo/models/base'
12
13
  require_relative 'printavo/models/customer'
14
+ require_relative 'printavo/models/status'
13
15
  require_relative 'printavo/models/order'
14
16
  require_relative 'printavo/models/job'
17
+ require_relative 'printavo/models/inquiry'
15
18
  require_relative 'printavo/resources/base'
16
19
  require_relative 'printavo/resources/customers'
20
+ require_relative 'printavo/resources/statuses'
17
21
  require_relative 'printavo/resources/orders'
18
22
  require_relative 'printavo/resources/jobs'
23
+ require_relative 'printavo/resources/inquiries'
19
24
  require_relative 'printavo/webhooks'
20
25
  require_relative 'printavo/client'
21
26
 
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Carver II
@@ -259,9 +259,12 @@ executables: []
259
259
  extensions: []
260
260
  extra_rdoc_files: []
261
261
  files:
262
- - CHANGELOG.md
263
262
  - LICENSE
264
263
  - README.md
264
+ - docs/CACHING.md
265
+ - docs/CHANGELOG.md
266
+ - docs/CONTRIBUTING.md
267
+ - docs/FUTURE.md
265
268
  - lib/printavo.rb
266
269
  - lib/printavo/client.rb
267
270
  - lib/printavo/config.rb
@@ -270,12 +273,17 @@ files:
270
273
  - lib/printavo/graphql_client.rb
271
274
  - lib/printavo/models/base.rb
272
275
  - lib/printavo/models/customer.rb
276
+ - lib/printavo/models/inquiry.rb
273
277
  - lib/printavo/models/job.rb
274
278
  - lib/printavo/models/order.rb
279
+ - lib/printavo/models/status.rb
280
+ - lib/printavo/page.rb
275
281
  - lib/printavo/resources/base.rb
276
282
  - lib/printavo/resources/customers.rb
283
+ - lib/printavo/resources/inquiries.rb
277
284
  - lib/printavo/resources/jobs.rb
278
285
  - lib/printavo/resources/orders.rb
286
+ - lib/printavo/resources/statuses.rb
279
287
  - lib/printavo/version.rb
280
288
  - lib/printavo/webhooks.rb
281
289
  homepage: https://github.com/scarver2/printavo-ruby
@@ -283,7 +291,7 @@ licenses:
283
291
  - MIT
284
292
  metadata:
285
293
  bug_tracker_uri: https://github.com/scarver2/printavo-ruby/issues
286
- changelog_uri: https://github.com/scarver2/printavo-ruby/blob/master/CHANGELOG.md
294
+ changelog_uri: https://github.com/scarver2/printavo-ruby/blob/master/docs/CHANGELOG.md
287
295
  documentation_uri: https://github.com/scarver2/printavo-ruby#readme
288
296
  source_code_uri: https://github.com/scarver2/printavo-ruby
289
297
  rubygems_mfa_required: 'true'
data/CHANGELOG.md DELETED
@@ -1,33 +0,0 @@
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